Repository: WJQSERVER-STUDIO/ghproxy
Pull request #108: Add support for multi-target docker image(oci) proxy (3.4.0)
Author: login: WJQSERVER
Created: 2025-05-21T01:04:12Z | Merged: 2025-05-21T08:24:09Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **新功能**  
  - 初步实现多 target 的 Docker 代理支持。  
  - 新增 weakcache 机制，用于处理短生命周期令牌（token）。  
  - 新增两个状态查询接口，分别用于检测 Shell 编辑器和 OCI 代理的启用状态。  
  - 支持 GHCR（GitHub Container Registry）专用的 HTTP 客户端和认证缓存机制。  
  - 新增 hub 主题支持。  

- **文档**  
  - Changelog 增加 v3.4.0 正式版及预发布说明，并警告勿在生产环境使用。  
  - README 更新使用示例，增加多种 Docker（OCI）代理镜像拉取示例。  

- **其他**  
  - 版本号更新至 3.4.0（25w40b）。
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 427ec1b..1666a28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
 # 更新日志
 
+3.4.0 - 2025-05-21
+---
+- ADD: 初步实现多`target` Docker代理
+- ADD: 加入`weakcache`用于处理短期令牌
+- ADD: 新增`hub`主题
+- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API
+
+25w40b - 2025-05-21
+---
+- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用;
+- ADD: 新增`hub`主题
+- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API
+- CHANGE: 对细节进行优化
+
+25w40a - 2025-05-21
+---
+- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用;
+- ADD: 初步实现多`target` Docker代理
+- ADD: 加入`weakcache`用于处理短期令牌
+
 3.3.3 - 2025-05-20
 ---
 - CHANGE: 加入`senseClientDisconnection`与`async`配置项
diff --git a/DEV-VERSION b/DEV-VERSION
index fdef6bc..9c87980 100644
--- a/DEV-VERSION
+++ b/DEV-VERSION
@@ -1 +1 @@
-25w39a
\ No newline at end of file
+25w40b
\ No newline at end of file
diff --git a/README.md b/README.md
index 9df6f2b..42815a0 100644
--- a/README.md
+++ b/README.md
@@ -37,11 +37,11 @@
 
 [GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
 
-[GHProxy项目文档Next(仍在建设中)](https://ghproxy-docs.pages.dev/)
+[GHProxy项目文档Next(仍在建设中)](https://wjqserver.pages.dev/docs/category/ghproxy) 感谢 [@redbunnys](https://github.com/redbunnys)的维护
 
 ### 使用示例
 
-```
+```bash 
 # 下载文件
 https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
 https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
@@ -49,6 +49,15 @@ https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/t
 # 克隆仓库
 git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
 git clone https://ghproxy.1888866.xyz/https://github.com/WJQSERVER-STUDIO/ghproxy.git
+
+# Docker(OCI) 代理
+docker pull gh.example.com/wjqserver/ghproxy
+docker pull gh.example.com/adguard/adguardhome
+
+docker pull gh.example.com/docker.io/wjqserver/ghproxy
+docker pull gh.example.com/docker.io/adguard/adguardhome
+
+docker pull gh.example.com/ghcr.io/openfaas/queue-worker 
 ```
 
 ## 部署说明
diff --git a/VERSION b/VERSION
index 3f09e91..fbcbf73 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.3.3
\ No newline at end of file
+3.4.0
\ No newline at end of file
diff --git a/api/api.go b/api/api.go
index 0c34ac0..e8ca10f 100644
--- a/api/api.go
+++ b/api/api.go
@@ -49,14 +49,18 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
 		apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
 			SmartGitStatusHandler(cfg, c, ctx)
 		})
-
+		apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) {
+			shellNestStatusHandler(cfg, c, ctx)
+		})
+		apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) {
+			ociProxyStatusHandler(cfg, c, ctx)
+		})
 	}
 	logInfo("API router Init success")
 }
 
 func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
 	sizeLimit := cfg.Server.SizeLimit
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"MaxResponseBodySize": sizeLimit,
@@ -64,7 +68,6 @@ func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Con
 }
 
 func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Whitelist": cfg.Whitelist.Enabled,
@@ -72,7 +75,6 @@ func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
 }
 
 func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Blacklist": cfg.Blacklist.Enabled,
@@ -80,7 +82,6 @@ func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
 }
 
 func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Cors": cfg.Server.Cors,
@@ -88,7 +89,6 @@ func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Co
 }
 
 func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Status": "OK",
@@ -96,7 +96,6 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
 }
 
 func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Version": version,
@@ -104,7 +103,6 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string)
 }
 
 func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"RateLimit": cfg.RateLimit.Enabled,
@@ -112,7 +110,6 @@ func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
 }
 
 func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"RatePerMinute": cfg.RateLimit.RatePerMinute,
@@ -120,9 +117,23 @@ func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx contex
 }
 
 func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"enabled": cfg.GitClone.Mode == "cache",
 	}))
 }
+
+func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
+	c.Response.Header.Set("Content-Type", "application/json")
+	c.JSON(200, (map[string]interface{}{
+		"enabled": cfg.Shell.Editor,
+	}))
+}
+
+func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
+	c.Response.Header.Set("Content-Type", "application/json")
+	c.JSON(200, (map[string]interface{}{
+		"enabled": cfg.Docker.Enabled,
+		"target":  cfg.Docker.Target,
+	}))
+}
diff --git a/main.go b/main.go
index 484d59f..2cbc5db 100644
--- a/main.go
+++ b/main.go
@@ -17,6 +17,7 @@ import (
 	"ghproxy/middleware/loggin"
 	"ghproxy/proxy"
 	"ghproxy/rate"
+	"ghproxy/weakcache"
 
 	"github.com/WJQSERVER-STUDIO/logger"
 	"github.com/hertz-contrib/http2/factory"
@@ -50,6 +51,10 @@ var (
 	pagesFS embed.FS
 )
 
+var (
+	wcache *weakcache.Cache[string] // docker token缓存
+)
+
 var (
 	logw       = logger.Logw
 	logDump    = logger.LogDump
@@ -208,6 +213,8 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
 		pages, err = fs.Sub(pagesFS, "pages/classic")
 	case "mino":
 		pages, err = fs.Sub(pagesFS, "pages/mino")
+	case "hub":
+		pages, err = fs.Sub(pagesFS, "pages/hub")
 	default:
 		pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题
 		logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
@@ -289,7 +296,7 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
 		staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
 	})
 	r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
-		staticServer := http.FileServer(http.FS(pages))
+		staticServer := http.FileServer(http.FS(assets))
 		req, err := adaptor.GetCompatRequest(&c.Request)
 		if err != nil {
 			logError("%s", err)
@@ -360,6 +367,9 @@ func init() {
 		setMemLimit(cfg)
 		loadlist(cfg)
 		setupRateLimit(cfg)
+		if cfg.Docker.Enabled {
+			wcache = proxy.InitWeakCache()
+		}
 
 		if cfg.Server.Debug {
 			runMode = "dev"
@@ -468,23 +478,7 @@ func main() {
 		proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
 	})
 
-	// for 3.4.0
-
-	/*
-		r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
-			proxy.GhcrRouting(cfg)(ctx, c)
-
-			/*
-				//proxy.GhcrRouting(cfg)(ctx, c)
-				// 返回200与空json
-				//c.JSON(200, map[string]interface{}{})
-				emptyJSON := "{}"
-				//emptyJSON := `{"name":"disable-list-tags","tags":[]}`
-				c.Header("Content-Type", "application/json")
-				c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
-				c.String(200, emptyJSON)
-	*/
-	/*
+	r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) {
 		emptyJSON := "{}"
 		c.Header("Content-Type", "application/json")
 		c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
@@ -493,30 +487,18 @@ func main() {
 
 		c.Status(200)
 		c.Write([]byte(emptyJSON))
-	*/
-
-	/*
-		w := adaptor.GetCompatResponseWriter(&c.Response)
+	})
 
-		const emptyJSON = "{}"
-		w.Header().Set("Content-Type", "application/json")
-		w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON)))
-		w.Header().Del("Server")
+	r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
+		proxy.GhcrWithImageRouting(cfg)(ctx, c)
+	})
 
-		fmt.Fprint(w, emptyJSON)
-	*/
 	/*
-		})
-
 		r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
 			proxy.GhcrRouting(cfg)(ctx, c)
 		})
 	*/
 
-	r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
-		proxy.GhcrRouting(cfg)(ctx, c)
-	})
-
 	r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
 		proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
 	})
@@ -530,8 +512,10 @@ func main() {
 			http.ListenAndServe("localhost:6060", nil)
 		}()
 	}
+	if wcache != nil {
+		defer wcache.StopCleanup()
+	}
 
-	r.Spin()
 	defer logger.Close()
 	defer func() {
 		if hertZfile != nil {
@@ -541,5 +525,8 @@ func main() {
 			}
 		}
 	}()
+
+	r.Spin()
+
 	fmt.Println("Program Exit")
 }
diff --git a/proxy/authparse.go b/proxy/authparse.go
new file mode 100644
index 0000000..f353db9
--- /dev/null
+++ b/proxy/authparse.go
@@ -0,0 +1,62 @@
+package proxy
+
+import (
+	"fmt"
+	"strings"
+)
+
+// BearerAuthParams 用于存放解析出的 Bearer 认证参数
+type BearerAuthParams struct {
+	Realm   string
+	Service string
+	Scope   string
+}
+
+// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
+// 它期望格式为 'Bearer key1="value1",key2="value2",...'
+// 并尝试将已知参数解析到 BearerAuthParams struct 中。
+func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
+	if headerValue == "" {
+		return nil, fmt.Errorf("header value is empty")
+	}
+
+	// 检查 Scheme 是否是 "Bearer"
+	parts := strings.SplitN(headerValue, " ", 2)
+	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+		return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
+	}
+	paramsStr := parts[1]
+
+	paramPairs := strings.Split(paramsStr, ",")
+	tempMap := make(map[string]string)
+
+	for _, pair := range paramPairs {
+		trimmedPair := strings.TrimSpace(pair)
+		keyValue := strings.SplitN(trimmedPair, "=", 2)
+		if len(keyValue) != 2 {
+			logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
+			continue
+		}
+		key := strings.TrimSpace(keyValue[0])
+		value := strings.TrimSpace(keyValue[1])
+		if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
+			value = value[1 : len(value)-1]
+		}
+		tempMap[key] = value
+	}
+
+	//从 map 中提取值并填充到 struct
+	authParams := &BearerAuthParams{}
+
+	if realm, ok := tempMap["realm"]; ok {
+		authParams.Realm = realm
+	}
+	if service, ok := tempMap["service"]; ok {
+		authParams.Service = service
+	}
+	if scope, ok := tempMap["scope"]; ok {
+		authParams.Scope = scope
+	}
+
+	return authParams, nil
+}
diff --git a/proxy/docker.go b/proxy/docker.go
index cdfc743..3f4c43a 100644
--- a/proxy/docker.go
+++ b/proxy/docker.go
@@ -2,39 +2,131 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
+
+/*
 func GhcrRouting(cfg *config.Config) app.HandlerFunc {
 	return func(ctx context.Context, c *app.RequestContext) {
-		if cfg.Docker.Enabled {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		path := ""
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			path = c.Param("filepath")
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, nil)
+
+	}
+}
+*/
+
+func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
+	return func(ctx context.Context, c *app.RequestContext) {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		reqImageUser := c.Param("user")
+		reqImageName := c.Param("repo")
+		reqFilePath := c.Param("filepath")
+
+		path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+			reqImageUser = c.Param("target")
+			reqImageName = c.Param("user")
+		}
+		image := &imageInfo{
+			User:  reqImageUser,
+			Repo:  reqImageName,
+			Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, image)
+
+	}
+
+}
+
+func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
+	if cfg.Docker.Enabled {
+		if target != "" {
+			GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target)
+		} else {
 			if cfg.Docker.Target == "ghcr" {
-				GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr")
+				GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget)
 			} else if cfg.Docker.Target == "dockerhub" {
-				GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub")
+				GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget)
 			} else if cfg.Docker.Target != "" {
 				// 自定义taget
-				GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom")
+				GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target)
 			} else {
 				// 配置为空
 				ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
 				return
 			}
-
-		} else {
-			ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
-			return
 		}
+
+	} else {
+		ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
+		return
 	}
 }
 
-func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
+func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) {
 
 	var (
 		method []byte
@@ -55,12 +147,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 
 	method = c.Request.Method()
 
-	rb := client.NewRequestBuilder(string(method), u)
+	rb := ghcrclient.NewRequestBuilder(string(method), u)
 	rb.NoDefaultHeaders()
 	rb.SetBody(c.Request.BodyStream())
 	rb.WithContext(ctx)
 
-	//req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
 	req, err = rb.Build()
 	if err != nil {
 		HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
@@ -73,14 +164,68 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 		req.Header.Add(headerKey, headerValue)
 	})
 
-	resp, err = client.Do(req)
+	req.Header.Set("Host", target)
+	if image != nil {
+		token, exist := cache.Get(image.Image)
+		if exist {
+			logDebug("Use Cache Token: %s", token)
+			req.Header.Set("Authorization", "Bearer "+token)
+		}
+	}
+
+	resp, err = ghcrclient.Do(req)
 	if err != nil {
 		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
 		return
 	}
 
-	// 错误处理(404)
-	if resp.StatusCode == 404 {
+	// 处理状态码
+	if resp.StatusCode == 401 {
+		// 请求target /v2/路径
+		if string(c.Request.URI().Path()) != "/v2/" {
+			resp.Body.Close()
+			if image == nil {
+				ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
+				return
+			}
+			token := ChallengeReq(target, image, ctx, c)
+
+			// 更新kv
+			if token != "" {
+				logDump("Update Cache Token: %s", token)
+				cache.Put(image.Image, token)
+			}
+
+			rb := ghcrclient.NewRequestBuilder(string(method), u)
+			rb.NoDefaultHeaders()
+			rb.SetBody(c.Request.BodyStream())
+			rb.WithContext(ctx)
+
+			req, err = rb.Build()
+			if err != nil {
+				HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+				return
+			}
+
+			c.Request.Header.VisitAll(func(key, value []byte) {
+				headerKey := string(key)
+				headerValue := string(value)
+				req.Header.Add(headerKey, headerValue)
+			})
+
+			req.Header.Set("Host", target)
+			if token != "" {
+				req.Header.Set("Authorization", "Bearer "+token)
+			}
+
+			resp, err = ghcrclient.Do(req)
+			if err != nil {
+				HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+				return
+			}
+		}
+
+	} else if resp.StatusCode == 404 { // 错误处理(404)
 		ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
 		return
 	}
@@ -101,8 +246,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 			bodySize = -1
 		}
 		if err == nil && bodySize > sizelimit {
-			var finalURL string
-			finalURL = resp.Request.URL.String()
+			finalURL := resp.Request.URL.String()
 			err = resp.Body.Close()
 			if err != nil {
 				logError("Failed to close response body: %v", err)
@@ -116,7 +260,6 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 	// 复制响应头，排除需要移除的 header
 	for key, values := range resp.Header {
 		for _, value := range values {
-			//c.Header(key, value)
 			c.Response.Header.Add(key, value)
 		}
 	}
@@ -136,3 +279,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 	c.SetBodyStream(bodyReader, -1)
 
 }
+
+type AuthToken struct {
+	Token string `json:"token"`
+}
+
+func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
+	var resp401 *http.Response
+	var req401 *http.Request
+	var err error
+
+	rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
+	rb401.NoDefaultHeaders()
+	rb401.WithContext(ctx)
+	rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
+	req401, err = rb401.Build()
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+		return
+	}
+	req401.Header.Set("Host", target)
+
+	resp401, err = ghcrclient.Do(req401)
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+		return
+	}
+	defer resp401.Body.Close()
+	bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
+	if err != nil {
+		logError("Failed to parse Www-Authenticate header: %v", err)
+		return
+	}
+
+	scope := fmt.Sprintf("repository:%s:pull", image.Image)
+
+	getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
+		NoDefaultHeaders().
+		WithContext(ctx).
+		AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ").
+		SetHeader("Host", bearer.Service).
+		AddQueryParam("service", bearer.Service).
+		AddQueryParam("scope", scope)
+
+	getAuthReq, err := getAuthRB.Build()
+	if err != nil {
+		logError("Failed to create request: %v", err)
+		return
+	}
+
+	authResp, err := ghcrclient.Do(getAuthReq)
+	if err != nil {
+		logError("Failed to send request: %v", err)
+		return
+	}
+
+	defer authResp.Body.Close()
+
+	bodyBytes, err := io.ReadAll(authResp.Body)
+	if err != nil {
+		logError("Failed to read auth response body: %v", err)
+		return
+	}
+
+	// 解码json
+	var authToken AuthToken
+	err = json.Unmarshal(bodyBytes, &authToken)
+	if err != nil {
+		logError("Failed to decode auth response body: %v", err)
+		return
+	}
+	token = authToken.Token
+
+	return token
+
+}
diff --git a/proxy/httpc.go b/proxy/httpc.go
index 83de29b..4d8bc4c 100644
--- a/proxy/httpc.go
+++ b/proxy/httpc.go
@@ -12,10 +12,12 @@ import (
 var BufferSize int = 32 * 1024 // 32KB
 
 var (
-	tr        *http.Transport
-	gittr     *http.Transport
-	client    *httpc.Client
-	gitclient *httpc.Client
+	tr         *http.Transport
+	gittr      *http.Transport
+	client     *httpc.Client
+	gitclient  *httpc.Client
+	ghcrtr     *http.Transport
+	ghcrclient *httpc.Client
 )
 
 func InitReq(cfg *config.Config) error {
@@ -23,11 +25,13 @@ func InitReq(cfg *config.Config) error {
 	if cfg.GitClone.Mode == "cache" {
 		initGitHTTPClient(cfg)
 	}
+	initGhcrHTTPClient(cfg)
 	err := SetGlobalRateLimit(cfg)
 	if err != nil {
 		return err
 	}
 	return nil
+
 }
 
 func initHTTPClient(cfg *config.Config) {
@@ -77,6 +81,7 @@ func initHTTPClient(cfg *config.Config) {
 			httpc.WithTransport(tr),
 		)
 	}
+
 }
 
 func initGitHTTPClient(cfg *config.Config) {
@@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
 		)
 	}
 }
+
+func initGhcrHTTPClient(cfg *config.Config) {
+	var proTolcols = new(http.Protocols)
+	proTolcols.SetHTTP1(true)
+	proTolcols.SetHTTP2(true)
+	if cfg.Httpc.Mode == "auto" {
+
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+			Protocols:       proTolcols,
+		}
+	} else if cfg.Httpc.Mode == "advanced" {
+		ghcrtr = &http.Transport{
+			MaxIdleConns:        cfg.Httpc.MaxIdleConns,
+			MaxConnsPerHost:     cfg.Httpc.MaxConnsPerHost,
+			MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
+			WriteBufferSize:     32 * 1024, // 32KB
+			ReadBufferSize:      32 * 1024, // 32KB
+			Protocols:           proTolcols,
+		}
+	} else {
+		// 错误的模式
+		logError("unknown httpc mode: %s", cfg.Httpc.Mode)
+		fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
+		logWarning("use Auto to Run HTTP Client")
+		fmt.Println("use Auto to Run HTTP Client")
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+		}
+	}
+	if cfg.Outbound.Enabled {
+		initTransport(cfg, ghcrtr)
+	}
+	if cfg.Server.Debug {
+		ghcrclient = httpc.New(
+			httpc.WithTransport(ghcrtr),
+			httpc.WithDumpLog(),
+		)
+	} else {
+		ghcrclient = httpc.New(
+			httpc.WithTransport(ghcrtr),
+		)
+	}
+}
diff --git a/proxy/match.go b/proxy/match.go
index 9db0c23..cee4eaa 100644
--- a/proxy/match.go
+++ b/proxy/match.go
@@ -17,9 +17,12 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
 	// 匹配 "https://github.com"开头的链接
 	if strings.HasPrefix(rawPath, "https://github.com") {
 		remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
-		if strings.HasPrefix(remainingPath, "/") {
-			remainingPath = strings.TrimPrefix(remainingPath, "/")
-		}
+		/*
+			if strings.HasPrefix(remainingPath, "/") {
+				remainingPath = strings.TrimPrefix(remainingPath, "/")
+			}
+		*/
+		remainingPath = strings.TrimPrefix(remainingPath, "/")
 		// 预期格式/user/repo/more...
 		// 取出user和repo和最后部分
 		parts := strings.Split(remainingPath, "/")
diff --git a/proxy/utils.go b/proxy/utils.go
index 21e7886..029dc26 100644
--- a/proxy/utils.go
+++ b/proxy/utils.go
@@ -13,8 +13,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
 
 	// 白名单检查
 	if cfg.Whitelist.Enabled {
-		var whitelist bool
-		whitelist = auth.CheckWhitelist(user, repo)
+		whitelist := auth.CheckWhitelist(user, repo)
 		if !whitelist {
 			ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
 			logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
@@ -24,8 +23,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri
 
 	// 黑名单检查
 	if cfg.Blacklist.Enabled {
-		var blacklist bool
-		blacklist = auth.CheckBlacklist(user, repo)
+		blacklist := auth.CheckBlacklist(user, repo)
 		if blacklist {
 			ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
 			logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
diff --git a/weakcache/weakcache.go b/weakcache/weakcache.go
new file mode 100644
index 0000000..32d897c
--- /dev/null
+++ b/weakcache/weakcache.go
@@ -0,0 +1,258 @@
+package weakcache
+
+import (
+	"container/list"
+	"sync"
+	"time"
+	"weak" // Go 1.24 引入的 weak 包
+)
+
+// DefaultExpiration 默认过期时间，这里设置为 15 分钟。
+// 这是一个导出的常量，方便用户使用包时引用默认值。
+const DefaultExpiration = 5 * time.Minute
+
+// cleanupInterval 是后台清理 Go routine 的扫描间隔，这里设置为 5 分钟。
+// 这是一个内部常量，不导出。
+const cleanupInterval = 2 * time.Minute
+
+// cacheEntry 缓存项的内部结构。不导出。
+type cacheEntry[T any] struct {
+	Value      T
+	Expiration time.Time
+	key        string // 存储key，方便在list.Element中引用
+}
+
+// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
+// 这是一个导出的类型。
+type Cache[T any] struct {
+	mu sync.RWMutex
+
+	// 修正：缓存存储：key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
+	// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
+	data map[string]weak.Pointer[cacheEntry[T]]
+
+	// FIFO 链表：存储 key 的 list.Element
+	// 链表头部是最近放入的，尾部是最早放入的（最老的）
+	fifoList *list.List
+	// FIFO 元素的映射：key -> *list.Element
+	fifoMap map[string]*list.Element
+
+	defaultExpiration time.Duration
+	maxSize           int // 缓存最大容量，0 表示无限制
+
+	stopCleanup chan struct{}
+	wg          sync.WaitGroup // 用于等待清理 Go routine 退出
+}
+
+// NewCache 创建一个新的缓存实例。
+// expiration: 新添加项的默认过期时间。如果为 0，则使用 DefaultExpiration。
+// maxSize: 缓存的最大容量，0 表示无限制。当达到上限时，采用 FIFO 策略淘汰。
+// 这是一个导出的构造函数。
+func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
+	if expiration <= 0 {
+		expiration = DefaultExpiration
+	}
+
+	c := &Cache[T]{
+		// 修正：初始化 map，值类型已修正
+		data:              make(map[string]weak.Pointer[cacheEntry[T]]),
+		fifoList:          list.New(),
+		fifoMap:           make(map[string]*list.Element),
+		defaultExpiration: expiration,
+		maxSize:           maxSize,
+		stopCleanup:       make(chan struct{}),
+	}
+	// 启动后台清理 Go routine
+	c.wg.Add(1)
+	go c.cleanupLoop()
+	return c
+}
+
+// Put 将值放入缓存。如果 key 已存在，会更新其值和过期时间。
+// 这是导出的方法。
+func (c *Cache[T]) Put(key string, value T) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	now := time.Now()
+	expiration := now.Add(c.defaultExpiration)
+
+	// 如果 key 已经存在，更新其值和过期时间。
+	// 在 FIFO 策略中， Put 更新不改变其在链表中的位置，除非旧的 entry 已经被 GC。
+	if elem, ok := c.fifoMap[key]; ok {
+		// 从 data map 中获取弱引用，wp 的类型现在是 weak.Pointer[cacheEntry[T]]
+		if wp, dataOk := c.data[key]; dataOk {
+			// wp.Value() 返回 *cacheEntry[T]， entry 的类型现在是 *cacheEntry[T]
+			entry := wp.Value()
+			if entry != nil {
+				// 旧的 cacheEntry 仍在内存中，直接更新
+				entry.Value = value
+				entry.Expiration = expiration
+				// 在严格 FIFO 中，更新不移动位置
+				return
+			}
+			// 如果 weak.Pointer.Value() 为 nil，说明之前的 cacheEntry 已经被 GC 了
+			// 此时需要创建一个新的 entry，并将其从旧位置移除，再重新添加
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		} else {
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		}
+	}
+
+	// 新建缓存项 (注意这里是结构体值，而不是指针)
+	// weak.Make 接收的是指针 *T
+	entry := &cacheEntry[T]{ // 创建结构体指针
+		Value:      value,
+		Expiration: expiration,
+		key:        key, // 存储 key
+	}
+
+	// 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map
+	// weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]]，类型匹配 data map 的值类型
+	c.data[key] = weak.Make(entry)
+
+	// 添加到 FIFO 链表头部 (最近放入/更新的在头部)
+	// PushFront 返回新的 list.Element
+	c.fifoMap[key] = c.fifoList.PushFront(key)
+
+	// 检查大小上限并进行淘汰 (淘汰尾部的最老项)
+	c.evictIfNeeded()
+}
+
+// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。
+// 这是导出的方法。
+func (c *Cache[T]) Get(key string) (T, bool) {
+	c.mu.RLock() // 先读锁
+	// 从 data map 中获取弱引用，wp 的类型现在是 weak.Pointer[cacheEntry[T]]
+	wp, ok := c.data[key]
+	c.mu.RUnlock() // 立即释放读锁，如果需要写操作（removeEntry）可以获得锁
+
+	var zero T // 零值
+
+	if !ok {
+		return zero, false
+	}
+
+	// 尝试获取实际的 cacheEntry 指针
+	// wp.Value() 返回 *cacheEntry[T]， entry 的类型现在是 *cacheEntry[T]
+	entry := wp.Value()
+
+	if entry == nil {
+		// 对象已被GC回收，需要清理此弱引用
+		c.removeEntry(key) // 内部会加写锁
+		return zero, false
+	}
+
+	// 检查过期时间 (通过 entry 指针访问字段)
+	if time.Now().After(entry.Expiration) {
+		// 逻辑上已过期
+		c.removeEntry(key) // 内部会加写锁
+		return zero, false
+	}
+
+	// 在 FIFO 缓存中，Get 操作不改变项在链表中的位置
+	return entry.Value, true // 通过 entry 指针访问值字段
+}
+
+// removeEntry 从缓存中移除项。
+// 这个方法是内部使用的，不导出。需要被调用者确保持有写锁，或者内部自己加锁。
+// 考虑到 Get 和 cleanupLoop 可能会调用，让其内部自己加锁更安全。
+func (c *Cache[T]) removeEntry(key string) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	// 从 data map 中删除
+	delete(c.data, key)
+
+	// 从 FIFO 链表和 fifoMap 中删除
+	if elem, ok := c.fifoMap[key]; ok {
+		c.fifoList.Remove(elem)
+		delete(c.fifoMap, key)
+	}
+}
+
+// evictIfNeeded 检查是否需要淘汰最老（FIFO 链表尾部）的项。
+// 这个方法是内部使用的，不导出。必须在持有写锁的情况下调用。
+func (c *Cache[T]) evictIfNeeded() {
+	if c.maxSize > 0 && c.fifoList.Len() > c.maxSize {
+		// 淘汰 FIFO 链表尾部的元素 (最老的)
+		oldest := c.fifoList.Back()
+		if oldest != nil {
+			keyToEvict := oldest.Value.(string) // 链表元素存储的是 key
+			c.fifoList.Remove(oldest)
+			delete(c.fifoMap, keyToEvict)
+			delete(c.data, keyToEvict) // 移除弱引用
+		}
+	}
+}
+
+// Size 返回当前缓存中的弱引用项数量。
+// 注意：这个数量可能包含已被 GC 回收但尚未清理的项。
+// 这是一个导出的方法。
+func (c *Cache[T]) Size() int {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+	return len(c.data)
+}
+
+// cleanupLoop 后台清理 Go routine。不导出。
+func (c *Cache[T]) cleanupLoop() {
+	defer c.wg.Done()
+	// 使用内部常量 cleanupInterval
+	ticker := time.NewTicker(cleanupInterval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			c.cleanupExpiredAndGCed()
+		case <-c.stopCleanup:
+			return
+		}
+	}
+}
+
+// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。
+func (c *Cache[T]) cleanupExpiredAndGCed() {
+	c.mu.Lock() // 清理时需要写锁
+	defer c.mu.Unlock()
+
+	now := time.Now()
+	keysToRemove := make([]string, 0, len(c.data)) // 预估容量
+
+	// 遍历 data map 查找需要清理的键
+	for key, wp := range c.data {
+		// wp 的类型是 weak.Pointer[cacheEntry[T]]
+		// wp.Value() 返回 *cacheEntry[T]， entry 的类型是 *cacheEntry[T]
+		entry := wp.Value() // 尝试获取强引用
+
+		if entry == nil {
+			// 已被 GC 回收
+			keysToRemove = append(keysToRemove, key)
+		} else if now.After(entry.Expiration) {
+			// 逻辑过期 (通过 entry 指针访问字段)
+			keysToRemove = append(keysToRemove, key)
+		}
+	}
+
+	// 执行删除操作
+	for _, key := range keysToRemove {
+		// 从 data map 中删除
+		delete(c.data, key)
+		// 从 FIFO 链表和 fifoMap 中删除
+		// 需要再次检查 fifoMap，因为在持有锁期间，evictIfNeeded 可能已经移除了这个 key
+		if elem, ok := c.fifoMap[key]; ok {
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		}
+	}
+}
+
+// StopCleanup 停止后台清理 Go routine。
+// 这是一个导出的方法。
+func (c *Cache[T]) StopCleanup() {
+	close(c.stopCleanup)
+	c.wg.Wait() // 等待 Go routine 退出
+}


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @WJQSERVER, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
As Gemini, I've reviewed this pull request which focuses on adding support for proxying Docker images (OCI) from multiple configurable targets. The core changes involve implementing the necessary authentication challenge flow required by many Docker registries and introducing a new weak cache mechanism to efficiently store temporary authentication tokens. A dedicated HTTP client configuration has also been added specifically for handling Docker registry interactions.

### Highlights
* **Multi-Target Docker Proxy**: Introduces new routing (`/v2/:target/:user/:repo/*filepath`) and underlying logic to allow proxying Docker/OCI images from different registry targets specified directly in the URL, in addition to the configured default target.
* **Docker Authentication Challenge Handling**: Adds logic to detect and respond to `401 Unauthorized` responses from Docker registries. It implements the standard authentication challenge flow by parsing the `Www-Authenticate` header, requesting a token from the specified auth server (`realm`) with the correct `service` and `scope`, and retrying the original request with the obtained bearer token.
* **Weak Cache for Tokens**: A new `weakcache` package is introduced and utilized to store temporary Docker authentication tokens. This cache leverages Go 1.24's `weak.Pointer` for memory efficiency and includes expiration and FIFO (First-In, First-Out) eviction based on a maximum size.
* **Dedicated HTTP Client for Docker**: A separate `http.Transport` and `httpc.Client` (`ghcrtr`, `ghcrclient`) are initialized specifically for making requests to Docker registries, allowing for potentially different configuration or monitoring compared to other proxy traffic.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **CHANGELOG.md**
  * Added entry for version 25w40a (v3.4.0 pre-release), noting the preliminary implementation of multi-target Docker proxy and the addition of `weakcache` for temporary tokens.
* **DEV-VERSION**
  * Updated the development version string to 25w40a.
* **main.go**
  * Imported the new `weakcache` package.
  * Declared and initialized a global `wcache` variable for Docker token caching, conditional on Docker being enabled in the configuration.
  * Removed commented-out code related to `/v2/` routing.
  * Added a new route handler for `/v2/:target/:user/:repo/*filepath` to support multi-target Docker image proxying.
  * Adjusted the program shutdown sequence to ensure the `wcache` cleanup goroutine is stopped gracefully before the program exits.
* **proxy/authparse.go**
  * New file added.
  * Contains the `BearerAuthParams` struct and the `parseBearerWWWAuthenticateHeader` function to parse the parameters from a `Www-Authenticate` header using the Bearer scheme.
* **proxy/docker.go**
  * Added constants for common Docker registry targets (`dockerhubTarget`, `ghcrTarget`).
  * Declared a package-level `cache` variable and `InitWeakCache` function to initialize the token cache.
  * Defined the `imageInfo` struct to hold parsed image details (user, repo, full image name).
  * Commented out the old `GhcrRouting` function.
  * Added the `GhcrWithImageRouting` function to handle the new multi-target route, parsing URL parameters and calling `GhcrToTarget`.
  * Added the `GhcrToTarget` function to centralize the logic for determining the target registry and calling `GhcrRequest`.
  * Modified `GhcrRequest` to use the new `ghcrclient` and set the `Host` header correctly.
  * Integrated the weak cache (`cache.Get`) to check for existing tokens before making the request.
  * Implemented logic to handle `401 Unauthorized` responses: if received (and not on the root `/v2/` path), it calls `ChallengeReq` to obtain a new token and retries the request with the token, caching the token (`cache.Put`).
  * Added the `AuthToken` struct for decoding the authentication response.
  * Added the `ChallengeReq` function to perform the Docker registry authentication challenge by making requests to the auth server specified in the `Www-Authenticate` header.
* **proxy/httpc.go**
  * Added `ghcrtr` and `ghcrclient` variables for a dedicated Docker HTTP client.
  * Added the `initGhcrHTTPClient` function to configure and initialize the dedicated Docker HTTP client, respecting `Httpc` and `Outbound` configurations.
  * Called `initGhcrHTTPClient` within the main `InitReq` function.
* **proxy/match.go**
  * Made a minor adjustment to the path trimming logic for URLs starting with `https://github.com`.
* **proxy/utils.go**
  * Made minor cosmetic changes to variable declarations (`var x bool = ...` changed to `x := ...`) in the `listCheck` function for whitelist and blacklist checks.
* **weakcache/weakcache.go**
  * New file added.
  * Implements a generic `Cache` type using Go 1.24's `weak.Pointer`.
  * Includes support for default expiration (`DefaultExpiration`), a background cleanup goroutine (`cleanupLoop`, `cleanupExpiredAndGCed`), and FIFO eviction based on a maximum size (`maxSize`, `evictIfNeeded`).
  * Provides `NewCache`, `Put`, `Get`, `Size`, and `StopCleanup` methods.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> Docker pulls arrive,
> Cache holds tokens, brief they live,
> Proxy helps them flow.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T01:04:49Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces support for multi-target Docker image (OCI) proxying and a new `weakcache` package for managing short-term tokens. The changes are substantial and add significant new functionality.

The implementation of the Docker proxy logic, including token authentication and caching, is comprehensive. The `weakcache` package provides a well-structured weak-reference based cache.

Several issues have been identified, including potential nil pointer dereferences which are critical for stability, and areas for improving maintainability and robustness. These should be addressed before merging.

### Summary of Findings
* **Potential Nil Pointer Dereference in main.go**: `wcache.StopCleanup()` in `main.go` may be called on a nil `wcache` if Docker support is disabled, leading to a panic. A nil check is recommended.
* **Potential Nil Pointer Dereference in proxy/docker.go**: Functions `GhcrRequest` and `ChallengeReq` in `proxy/docker.go` access `image.Image` without checking if `image` is nil. While current call paths might prevent this, the functions themselves are not robust and could panic if called with a nil `image` in the future. Nil checks are recommended within these functions.
* **Inconsistent HTTP Transport Configuration**: In `proxy/httpc.go`, the fallback HTTP transport for an unknown `cfg.Httpc.Mode` in `initGhcrHTTPClient` does not set custom protocols, differing from other modes. This could lead to inconsistent behavior.
* **Code Duplication in HTTP Client Initialization**: There's significant code duplication across `initHTTPClient`, `initGitHTTPClient`, and the new `initGhcrHTTPClient` in `proxy/httpc.go`. Refactoring into a common helper function would improve maintainability.

### Merge Readiness
This pull request introduces valuable new features. However, there are a few high-severity issues related to potential nil pointer dereferences that could lead to panics, and some medium-severity issues concerning code maintainability and robustness. 

I recommend addressing at least the high-severity issues before merging this PR to ensure stability. The medium-severity issues should also be considered for improvement. 

As an AI, I am not authorized to approve pull requests. Please ensure further review and approval from authorized team members after addressing the concerns.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T01:06:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🔭 Outside diff range comments (3)</summary><blockquote>

<details>
<summary>main.go (2)</summary><blockquote>

`479-488`: _⚠️ Potential issue_

**`/v2/` 仅处理 GET，未覆盖 HEAD/OPTIONS**

Docker 客户端启动阶段会对根路径发起 `HEAD /v2/` 探针；若 405 将导致镜像拉取失败。  
可改用 `r.Any("/v2/", ...)` 或显式增加 `r.HEAD`、`r.OPTIONS`。

---

`514-526`: _⚠️ Potential issue_

**在 Docker 功能关闭时可能触发空指针崩溃**

`wcache` 仅在 `cfg.Docker.Enabled` 为真时创建，但无条件执行 `defer wcache.StopCleanup()`。  
当 Docker 功能关闭时，`wcache == nil`，调用其方法必然 panic。

```diff
-   defer wcache.StopCleanup()
+   if wcache != nil {
+       defer wcache.StopCleanup()
+   }
```

请尽快修复。

</blockquote></details>
<details>
<summary>proxy/docker.go (1)</summary><blockquote>

`162-174`: _⚠️ Potential issue_

**复制请求头存在重复及敏感信息泄漏风险**

1. 使用 `Header.Add` 会把所有原始头逐字复制，随后又 `Set` 新值，造成同名头重复（特别是 `Host`、`Authorization`）。  
2. 若客户端本身携带 `Authorization`，该凭据会被转发到第三方镜像仓库，存在泄漏风险。

改进建议：

```diff
-    c.Request.Header.VisitAll(func(key, value []byte) {
-        req.Header.Add(string(key), string(value))
-    })
+    c.Request.Header.VisitAll(func(key, value []byte) {
+        k := strings.ToLower(string(key))
+        if k == "authorization" || k == "host" {
+            return // 跳过敏感或将被覆盖的头
+        }
+        req.Header.Set(string(key), string(value))
+    })
```

并在下方统一 `Set("Authorization", …)`，保证只有一份凭据。

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>CHANGELOG.md (1)</summary><blockquote>

`1-3`: **格式问题：请使用一致的标题风格**

Markdown 文件中的标题格式应该保持一致。根据静态分析工具的提示，应该使用 ATX 风格的标题（使用 `#` 符号）而不是 Setext 风格（使用下划线）。


```diff
-# 更新日志
-
+# 更新日志
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

3-3: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

</blockquote></details>
<details>
<summary>proxy/httpc.go (3)</summary><blockquote>

`14-21`: **全局变量过多，建议提供封装接口而非直接暴露包级可变状态**

`ghcrtr` / `ghcrclient` 等与现有 `tr` / `client` 一样都是包级可写变量。  
随着 target 数量的增加，包内公共可变状态会急剧膨胀，后期容易出现：

1. 并发读写竞态（目前尚未加锁保护）。  
2. 难以在单元测试中做隔离／Mock。  
3. 代码阅读者必须了解所有全局变量的生命周期。

可以考虑：

```diff
-type ghcrclient *httpc.Client
+// 建议：定义一个 struct 保存多组 client，提供只读 getter。
+type clients struct {
+    default *httpc.Client
+    git     *httpc.Client
+    ghcr    *httpc.Client
+}
+
+var c clients
+
+func GetGhcrClient() *httpc.Client { return c.ghcr }
```

通过集中管理并提供只读 accessor，可显著降低耦合度及潜在数据竞争。

---

`28-33`: **在未启用 Docker 功能时仍初始化 GHCR Client，可能造成不必要的资源占用**

当前无论 `cfg.Docker.Enabled` 开关如何，都会调用 `initGhcrHTTPClient` 并创建 Transport 及 goroutine（HTTP/2 pings 等）。若用户仅部署 Git / HTTP 代理，初始化 GHCR 无意义。可按下列方式按需延迟：

```diff
-    initGhcrHTTPClient(cfg)
+    if cfg.Docker.Enabled {
+        initGhcrHTTPClient(cfg)
+    }
```

既能节省文件描述符，也避免后续对 `ghcrclient` 的误用。

---

`156-202`: **`initGhcrHTTPClient` 与 `initHTTPClient`/`initGitHTTPClient` 重复度高，且存在小瑕疵**

1. 大段配置判定逻辑基本一致，可抽取公共构造函数，避免三处重复维护导致配置不一致。  
2. 变量名 `proTolcols` 有拼写错误，建议统一为 `protoCfg`（或其他可读性更好的名称）。  
3. 与 `initHTTPClient` 相比，此处未 `SetUnencryptedHTTP2(true)`，导致 h2c 在 GHCR 路径下被禁用，若上游支持 h2c 将退回 http/1.1，性能受损——请确认是否有意为之。  
4. 建议将多处硬编码的 `32 * 1024` 换成文件顶部的 `BufferSize` 常量，保持一致性。

可先抽象：

```go
func newTransport(cfg *config.Config, proto *http.Protocols) *http.Transport { ... }
```

然后三处仅做差异化开关，使维护成本最小化。

</blockquote></details>
<details>
<summary>main.go (1)</summary><blockquote>

`54-56`: **全局 `wcache` 未封装，未来易与业务代码耦合**

建议把弱缓存隐藏在 `proxy` 包内部，暴露对 token 的 `GetToken/PutToken` API，而非在 `main` 中直接持有指针。

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f3eb92ea51bd123ae18e42277c4e42a94060a5bc and c7954ae91aba1eb947f0fd0b0df1f6c977637083.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `CHANGELOG.md` (1 hunks)
* `DEV-VERSION` (1 hunks)
* `main.go` (7 hunks)
* `proxy/authparse.go` (1 hunks)
* `proxy/docker.go` (5 hunks)
* `proxy/httpc.go` (3 hunks)
* `proxy/match.go` (1 hunks)
* `proxy/utils.go` (2 hunks)
* `weakcache/weakcache.go` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>proxy/httpc.go (1)</summary>

```
Learnt from: WJQSERVER
PR: WJQSERVER-STUDIO/ghproxy#95
File: proxy/docker.go:3-11
Timestamp: 2025-04-27T08:55:17.730Z
Learning: In the ghproxy project, the HTTP client used for proxy requests is defined as a package-level variable `client` in the proxy package (in httpc.go) and can be accessed by all files in the same package without requiring additional imports.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>proxy/utils.go (2)</summary><blockquote>

<details>
<summary>auth/whitelist.go (1)</summary>

* `CheckWhitelist` (62-83)

</details>
<details>
<summary>auth/blacklist.go (1)</summary>

* `CheckBlacklist` (61-82)

</details>

</blockquote></details>
<details>
<summary>proxy/docker.go (3)</summary><blockquote>

<details>
<summary>weakcache/weakcache.go (3)</summary>

* `Cache` (27-45)
* `NewCache` (51-69)
* `DefaultExpiration` (12-12)

</details>
<details>
<summary>config/config.go (1)</summary>

* `Config` (9-22)

</details>
<details>
<summary>proxy/error.go (3)</summary>

* `ErrorPage` (136-145)
* `NewErrorWithStatusLookup` (88-105)
* `HandleError` (23-26)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>CHANGELOG.md</summary>

3-3: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>DEV-VERSION (1)</summary>

`1-1`: **版本号更新至 25w40a**

版本号从 "25w39a" 更新到 "25w40a"，对应本次引入的多 target Docker 代理支持和短时 token 缓存机制（weakcache）的功能增强。

</details>
<details>
<summary>CHANGELOG.md (1)</summary>

`3-8`: **预发布版本的更新日志看起来不错**

新增的更新日志条目清晰地描述了本次预发布版本（v3.4.0）的主要功能：
1. 初步实现多 `target` Docker 代理
2. 加入 `weakcache` 用于处理短期令牌

同时也适当标注了这是预发布版本，提醒用户不要在生产环境中使用。

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

3-3: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

</details>
<details>
<summary>proxy/match.go (1)</summary>

`20-25`: **代码简化，移除条件检查**

将带条件的前缀检查和移除操作简化为无条件移除。这是合理的优化，因为 `strings.TrimPrefix` 在前缀不存在时会安全地返回原始字符串。此更改简化了代码并保持了一致的路径处理行为。

此更改与新增的多 target Docker 代理支持相匹配，确保了一致的路径解析方式，支持 GHCR 镜像的路由和请求处理。

</details>
<details>
<summary>proxy/utils.go (2)</summary>

`16-17`: **简化变量声明的代码风格改进**

将变量声明和赋值合并为短变量声明（`:=`），使代码更简洁。这是纯粹的代码风格优化，不影响功能，遵循了 Go 语言的惯用写法。

---

`26-27`: **简化变量声明的代码风格改进**

与白名单检查类似，将变量声明和赋值合并为短变量声明（`:=`），使代码更简洁。这是纯粹的代码风格优化，不影响功能，遵循了 Go 语言的惯用写法。

</details>
<details>
<summary>main.go (1)</summary>

`491-493`: **多 target 路由缺少鉴权/限流中间件，与其他代理入口不一致**

其它 `RoutingHandler` 均包裹了 `limiter` / `iplimiter`，而 GHCR 路径直接调用 `GhcrWithImageRouting`，可能绕过全局限流。  
请确认是否需要与 main 入口层面的中间件保持一致。

</details>
<details>
<summary>weakcache/weakcache.go (1)</summary>

`7-8`: **`weak` 包为 Go 1.24 实验特性，请确认编译及部署环境**

当前 Go 最新稳定版尚未内置 `weak` 标准库（仅在 dev branch 可用）。  
若 CI/CD 仍使用 1.22/1.23，将编译失败。请确认：

1. 构建镜像的 `go` 版本 ≥ 1.24beta。  
2. `go.mod` 中的 `go` 字段同步更新。  
3. 发布说明中注明最低版本要求。

如需兼容旧版本，可考虑使用 `github.com/cespare/xxhash/v2` + `runtime.SetFinalizer` 方案或第三方 weakref 实现。

</details>
<details>
<summary>proxy/docker.go (1)</summary>

`295-318`: **`Host` 头设置与实际域名不一致**

`ChallengeReq()` 中：

```go
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
...
req401.Header.Set("Host", target)
```

若 `target` 是 `registry-1.docker.io`，但真实 token 服务器域名为 `auth.docker.io`（常见情况），错误的 `Host` 头会导致 404 或 SNI 失败。建议直接让 HTTP Client 依据 URL 自动填充 `Host`，或解析 `bearer.Realm` 后再精准设置。

```diff
- req401.Header.Set("Host", target)
+ // 让 http.Client 根据 URL 自动决定 Host，除非有特殊需求
```

同理，后续获取 token 的请求也应保证 `Host` 与 `bearer.Realm` 域名一致。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T01:07:37Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>api/api.go (2)</summary><blockquote>

`126-131`: **函数命名风格与项目不一致**

函数 `shellNestStatusHandler` 使用了驼峰命名法（camelCase），而项目中其他处理函数如 `HealthcheckHandler`、`VersionHandler` 等都使用了帕斯卡命名法（PascalCase）。建议保持命名风格一致。

```diff
-func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
+func ShellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
```

相应地，也需要更新调用处：
```diff
 apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) {
-    shellNestStatusHandler(cfg, c, ctx)
+    ShellNestStatusHandler(cfg, c, ctx)
 })
```

---

`133-139`: **函数命名风格不一致且返回的 JSON 结构与其他端点不同**

1. 函数 `ociProxyStatusHandler` 使用了驼峰命名法（camelCase），与项目中其他处理函数的命名风格不一致。

2. 返回的 JSON 中同时包含了 `"enabled"` 和 `"target"` 字段，而大多数其他状态处理函数返回的是一个与功能名称对应的单字段（如 `"Whitelist": cfg.Whitelist.Enabled`）。虽然这种设计也是合理的，但与现有的 API 模式存在差异。


```diff
-func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
+func OciProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
```

相应地，也需要更新调用处：
```diff
 apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) {
-    ociProxyStatusHandler(cfg, c, ctx)
+    OciProxyStatusHandler(cfg, c, ctx)
 })
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c7954ae91aba1eb947f0fd0b0df1f6c977637083 and 31d435bfa0acafc21eb89ad6270535f35b290b84.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `api/api.go` (1 hunks)
* `main.go` (9 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* main.go

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>api/api.go (1)</summary>

`52-57`: **添加新状态端点以支持多目标 Docker 代理**

新添加的两个状态端点 `/shell_nest/status` 和 `/oci_proxy/status` 扩展了 API 功能，符合现有的路由结构模式。这些端点能够提供运行时的功能状态信息，有助于监控和诊断。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T03:57:52Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>proxy/docker.go (5)</summary><blockquote>

`24-35`: **全局缓存可能未初始化导致空指针**

`cache` 是包级变量，但只有在显式调用 `InitWeakCache()` 后才会被赋值。
如果上层（如 `main.go`）忘记调用该函数，`GhcrRequest()` 中的 `cache.Get()` 将触发 panic。

建议做惰性初始化，确保缓存存在：

```diff
var cache *weakcache.Cache[string]

+func getCache() *weakcache.Cache[string] {
+	if cache == nil {
+		cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	}
+	return cache
+}
```

并将所有 `cache.Get/Put` 调用改为 `getCache().Get/Put`。

---

`75-75`: **路径拼接可能出现重复 `/`**

`reqFilePath` 在路由规则里通常已包含前导 `/`。
当前直接使用 `fmt.Sprintf("%s/%s/%s", ...)` 可能生成包含双斜杠的路径（如 `user/repo//manifest`），导致后端 404。

```diff
-path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+cleanFilePath := strings.TrimPrefix(reqFilePath, "/")
+path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, cleanFilePath)
```

---

`107-107`: **QueryString 缺少 `?` 前缀检查**

直接拼接 `c.Request.QueryString()` 到 URL 会导致问题：

1. 当查询字符串为空时，会产生以 `?` 结尾的 URL
2. 当查询字符串不为空时，未添加 `?` 前缀时可能被误解析为路径

```diff
-GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target)
+qs := c.Request.QueryString()
+url := "https://" + target + "/v2/" + path
+if len(qs) > 0 {
+    url += "?" + string(qs)
+}
+GhcrRequest(ctx, c, url, image, cfg, target)
```

其他类似的 URL 构建点也需要做相同处理。

---

`315-315`: **使用 image.Image 前需添加 nil 检查**

函数 `ChallengeReq` 在调用前（191 行）已经做了 image 非空检查，但在函数内部直接使用 `image.Image` 仍存在风险。建议添加内部检查以增强健壮性：

```diff
+	if image == nil {
+		logError("ChallengeReq called with nil imageInfo")
+		return ""
+	}
	scope := fmt.Sprintf("repository:%s:pull", image.Image)
```

这样即使上层调用时忽略了非空检查，函数自身也能避免崩溃。

---

`183-227`: _🛠️ Refactor suggestion_

**401 重新发起请求时存在重复代码逻辑**

处理 401 响应时，重建请求的代码块（199-219 行）与前面的请求构建代码（150-167 行）几乎完全相同，这带来维护负担。

建议提炼成内部函数，减少重复：

```diff
+func buildUpstreamRequest(ctx context.Context, c *app.RequestContext, method []byte, u string, target string, token string) (*http.Request, error) {
+	rb := ghcrclient.NewRequestBuilder(string(method), u)
+	rb.NoDefaultHeaders()
+	rb.SetBody(c.Request.BodyStream())
+	rb.WithContext(ctx)
+
+	req, err := rb.Build()
+	if err != nil {
+		return nil, err
+	}
+
+	c.Request.Header.VisitAll(func(key, value []byte) {
+		headerKey := string(key)
+		// 跳过敏感头，避免复制原始凭据
+		if !strings.EqualFold(headerKey, "Authorization") {
+			headerValue := string(value)
+			req.Header.Add(headerKey, headerValue)
+		}
+	})
+
+	req.Header.Set("Host", target)
+	if token != "" {
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+
+	return req, nil
+}
```

然后在两处调用这个函数。

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>proxy/docker.go (2)</summary><blockquote>

`283-285`: **考虑扩展 AuthToken 结构以支持更完整的响应格式**

当前 `AuthToken` 结构只包含 `token` 字段，但 Docker Registry 认证 API 通常还会返回其他有用信息，如：

```diff
type AuthToken struct {
	Token       string `json:"token"`
+	ExpiresIn   int    `json:"expires_in,omitempty"`
+	IssuedAt    string `json:"issued_at,omitempty"`
+	AccessToken string `json:"access_token,omitempty"`
}
```

扩展结构体可以在未来支持更高级的功能，比如基于过期时间做更智能的缓存管理。

---

`107-116`: **请统一 URL 构建逻辑**

注意到有多处 URL 构建方式：

1. Line 107: `"https://"+target+"/v2/"+path+"?"+string(...)`
2. Line 110: `"https://"+ghcrTarget+string(c.Request.RequestURI())`
3. Line 112: `"https://"+dockerhubTarget+string(c.Request.RequestURI())`
4. Line 115: `"https://"+cfg.Docker.Target+string(c.Request.RequestURI())`

建议提炼出统一的 URL 构建函数，确保所有的构建逻辑保持一致，尤其是查询参数的处理方式：

```go
func buildDockerApiUrl(host string, path string, query []byte) string {
    url := fmt.Sprintf("https://%s%s", host, path)
    if len(query) > 0 {
        url += "?" + string(query)
    }
    return url
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 31d435bfa0acafc21eb89ad6270535f35b290b84 and 430e313d4739b374ac03425bd47d47c44ead0508.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `proxy/docker.go` (5 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>proxy/docker.go (3)</summary><blockquote>

<details>
<summary>weakcache/weakcache.go (3)</summary>

* `Cache` (27-45)
* `NewCache` (51-69)
* `DefaultExpiration` (12-12)

</details>
<details>
<summary>config/config.go (1)</summary>

* `Config` (9-22)

</details>
<details>
<summary>proxy/error.go (3)</summary>

* `ErrorPage` (136-145)
* `NewErrorWithStatusLookup` (88-105)
* `HandleError` (23-26)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>proxy/docker.go (3)</summary>

`169-174`: **需要检查 image 是否为 nil**

在访问 `image.Image` 前应先检查 `image` 是否为 `nil`，否则可能导致空指针 panic。

```diff
-if image != nil {
-	token, exist := cache.Get(image.Image)
-	if exist {
-		logDebug("Use Cache Token: %s", token)
-		req.Header.Set("Authorization", "Bearer "+token)
-	}
-}
+if image != nil {
+	token, exist := cache.Get(image.Image)
+	if exist {
+		logDebug("Use Cache Token: %s", token)
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+}
```

虽然已有 `if image != nil` 检查，但这是一个重要的安全检查，值得确认保留。

---

`187-191`: **验证 image 非空的检查已正确添加**

代码现在正确检查了 `image` 是否为 `nil`，防止空指针异常，这是一个良好的修复。

当 `image` 为 `nil` 时无法进行 Bearer 认证，直接返回 401 错误给客户端是合理的处理方式。

---

`66-102`: **GhcrWithImageRouting 路由解析逻辑清晰**

新增的镜像路由解析函数逻辑清晰，能够有效提取请求中的镜像用户/仓库信息并构造 imageInfo 对象，支持了多 target 代理功能。

函数结构良好，解析方式符合 Docker 规范。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T04:11:34Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces significant new functionality by adding multi-target Docker image (OCI) proxy support, along with a `weakcache` for managing short-lived tokens and dedicated HTTP client handling for GHCR. The changes are quite extensive and touch upon routing, API, HTTP client management, and introduce new caching and authentication parsing logic.

Overall, the additions are well-structured. The new `weakcache` and `authparse` packages are good steps towards modularity. The OCI proxy logic in `proxy/docker.go` is complex but appears to cover the necessary authentication flow. 

There are a few areas for potential improvement, particularly around error handling, logging, and some code clarity, which I've detailed in the specific comments. Given the complexity and the pre-release nature, thorough testing will be crucial.

### Summary of Findings
* **Logging Removal**: Basic access logging was removed from several API handlers in `api/api.go`. This might impact monitoring/debugging capabilities if the removal was unintentional or if no alternative logging is in place for these endpoints. (Severity: medium, commented)
* **Favicon Asset Path**: The source for `/favicon.ico` in `main.go` was changed from `pagesFS` to `assetsFS`. This needs to be consistent with how assets are structured and loaded. (Severity: medium, commented)
* **Commented-Out Code**: Significant blocks of commented-out routing logic exist in `main.go`, which could be cleaned up. (Severity: medium, commented)
* **WeakCache Cleanup Deferral**: `wcache.StopCleanup()` in `main.go` is deferred before `r.Spin()`, potentially delaying cleanup until hard termination. (Severity: medium, commented)
* **Error Message Verbosity in Auth Parsing**: Error messages in `proxy/authparse.go` include the full header value, which could be a minor information disclosure risk. (Severity: medium, commented)
* **Handling of Malformed Auth Parameters**: `parseBearerWWWAuthenticateHeader` logs warnings for malformed parameters but doesn't signal this partial failure to the caller via an error. (Severity: medium, commented)
* **Global Cache Variable**: `InitWeakCache` in `proxy/docker.go` initializes a global cache, which can complicate testing. Dependency injection could be an alternative. (Severity: medium, commented)
* **OCI Proxy URL Parsing Logic**: The fallback URL parsing logic in `GhcrWithImageRouting` (when `reqTarget` doesn't contain '.') seems potentially problematic and might lead to incorrect image information. (Severity: high, commented)
* **URL Query String Concatenation**: Appending query strings in `GhcrToTarget` might lead to malformed URLs if the base path already contains a query string. (Severity: medium, commented)
* **OCI Proxy 401 Handling and Retry Logic**: The 401 handling in `GhcrRequest` has complex conditions and duplicates request creation logic. Refactoring could improve clarity and maintainability. (Severity: medium, commented)
* **Hardcoded User-Agent**: A specific Docker client User-Agent is hardcoded in `ChallengeReq`. This could become outdated and might benefit from configurability or a comment explaining its necessity. (Severity: medium, commented)
* **Error Propagation from Token Challenge**: `ChallengeReq` returns an empty token on failure without a distinct error, potentially leading to silent failures or repeated 401s in the caller. (Severity: high, commented)
* **HTTP Client Initialization Fallback Logging**: Using `fmt.Println` for error/warning messages in `proxy/httpc.go` during client initialization bypasses the configured logger. (Severity: medium, commented)
* **WeakCache DefaultExpiration Comment**: Comment for `DefaultExpiration` in `weakcache/weakcache.go` states 15 minutes, but the value is 5 minutes. (Severity: medium, commented)
* **WeakCache Put Behavior (FIFO/LRU)**: The `Put` method in `weakcache` has a mixed behavior for FIFO when an entry was GC'd (re-added to front) versus updated in place (maintains FIFO position). Clarity on the intended eviction policy would be good. (Severity: medium, commented)
* **WeakCache Get Method Lock Contention**: The `Get` method in `weakcache` might lead to minor lock contention when multiple goroutines try to remove the same expired/GC'd key. (Severity: medium, commented)
* **Changelog Clarity**: The changelog entries are clear and concise. (Severity: low, not commented due to settings)
* **Variable Shadowing/Reassignment**: In `proxy/utils.go`, `var whitelist bool; whitelist = ...` was changed to `whitelist := ...`. This is a good simplification. (Severity: low, not commented due to settings)
* **Code Comments**: The new `weakcache` and `authparse` packages are well-commented, explaining the purpose of types and functions. (Severity: low, not commented due to settings)

### Merge Readiness
This pull request introduces substantial and complex new features. While the overall structure is good, there are several `high` and `medium` severity issues identified, particularly concerning URL parsing in the OCI proxy, error propagation from token acquisition, and clarity in caching behavior. 

I recommend that these issues, especially the `high` severity ones, be addressed before merging. Given the pre-release nature (v3.4.0), thorough testing of the new OCI proxy functionality across various registries and URL patterns will be critical. 

As an AI, I am not authorized to approve pull requests. Please ensure further review and approval from authorized team members after addressing the feedback.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T04:15:04Z
user: login: WJQSERVER
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T04:22:24Z
user: login: WJQSERVER
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T04:24:40Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>CHANGELOG.md (1)</summary><blockquote>

`3-9`: **Release 条目已添加，建议统一“新增”与“加入”术语**  
当前第5行至第8行混用“初步实现”、“加入”、“新增”表述，建议统一为“新增”以保持全篇一致性，例如将“加入`weakcache`用于处理短期令牌”改为“新增`weakcache`用于处理短期令牌”。

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

3-3: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 430e313d4739b374ac03425bd47d47c44ead0508 and 1afb3521942a952b57291086889b2d0db2af4017.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `CHANGELOG.md` (1 hunks)
* `DEV-VERSION` (1 hunks)
* `VERSION` (1 hunks)
* `main.go` (9 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* VERSION

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* DEV-VERSION
* main.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>CHANGELOG.md</summary>

3-3: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

---

10-10: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

---

17-17: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>CHANGELOG.md (2)</summary>

`10-15`: **`25w40b` 预发布条目格式正确**  
条目顺序（版本→分隔线→说明列表）、内容与既有风格保持一致，功能点覆盖完整，无需改动。

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

10-10: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

---

`17-22`: **`25w40a` 预发布条目格式正确**  
条目结构与历史预发布条目一致，功能描述清晰，建议保留现状。

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

17-17: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-21T07:27:19Z

Review comments:
diff_hunk: @@ -531,7 +511,7 @@ func main() {
 		}()
 	}
 
-	r.Spin()
+	defer wcache.StopCleanup()
path: main.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `wcache` variable is initialized conditionally within the `init()` function (only if `cfg.Docker.Enabled` is true). If Docker support is disabled, `wcache` will remain `nil`. Calling `StopCleanup()` on a nil `wcache` will cause a panic when the program exits.

Could we add a nil check here to prevent this potential panic?

```go
	if wcache != nil {
		defer wcache.StopCleanup()
	}
```
created_at: 2025-05-21T01:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 		req.Header.Add(headerKey, headerValue)
 	})
 
-	resp, err = client.Do(req)
+	req.Header.Set("Host", target)
+	token, exist := cache.Get(image.Image)
+	if exist {
+		logDebug("Use Cache Token: %s", token)
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `image` parameter might be `nil` if `GhcrRequest` is called from a path that doesn't provide image details (e.g., if the now-commented-out `GhcrRouting` were used, or if `GhcrToTarget` is called with `image = nil` under certain conditions).
Accessing `image.Image` here without a nil check for `image` could lead to a panic.

To make this function more robust, would it be possible to add a check for `image != nil` before attempting to use `image.Image` for the cache key?

```suggestion
	if image != nil {
		token, exist := cache.Get(image.Image)
		if exist {
			logDebug("Use Cache Token: %s", token)
			req.Header.Set("Authorization", "Bearer "+token)
		}
	}
```
created_at: 2025-05-21T01:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 		req.Header.Add(headerKey, headerValue)
 	})
 
-	resp, err = client.Do(req)
+	req.Header.Set("Host", target)
+	token, exist := cache.Get(image.Image)
+	if exist {
+		logDebug("Use Cache Token: %s", token)
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+
+	resp, err = ghcrclient.Do(req)
 	if err != nil {
 		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
 		return
 	}
 
-	// 错误处理(404)
-	if resp.StatusCode == 404 {
+	// 处理状态码
+	if resp.StatusCode == 401 {
+		// 请求target /v2/路径
+		if string(c.Request.URI().Path()) != "/v2/" {
+			resp.Body.Close()
+			token := ChallengeReq(target, image, ctx, c)
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

Similar to the cache access, `ChallengeReq` is called with `image`. If `image` is `nil`, the `ChallengeReq` function will attempt to access `image.Image` (e.g., in `fmt.Sprintf("repository:%s:pull", image.Image)`), leading to a panic.

Could we add a check here: if `image` is `nil` before calling `ChallengeReq`, perhaps handle it as an error or skip the challenge if image information is essential for it?

```suggestion
			if image == nil {
				HandleError(c, "cannot perform auth challenge without image information")
				// Consider returning or handling this error appropriately
				// For now, let's assume we might need to prevent further execution if image is nil and required.
                // If ChallengeReq can handle a nil image gracefully (e.g., by returning an error or empty token),
                // then this specific check might be less critical here, but ChallengeReq itself must be robust.
			} else {
                resp.Body.Close()
                token := ChallengeReq(target, image, ctx, c)
            }
```
created_at: 2025-05-21T01:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,3 +274,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 	c.SetBodyStream(bodyReader, -1)
 
 }
+
+type AuthToken struct {
+	Token string `json:"token"`
+}
+
+func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
+	var resp401 *http.Response
+	var req401 *http.Request
+	var err error
+
+	rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
+	rb401.NoDefaultHeaders()
+	rb401.WithContext(ctx)
+	rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
+	req401, err = rb401.Build()
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+		return
+	}
+	req401.Header.Set("Host", target)
+
+	resp401, err = ghcrclient.Do(req401)
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+		return
+	}
+	defer resp401.Body.Close()
+	bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
+	if err != nil {
+		logError("Failed to parse Www-Authenticate header: %v", err)
+		return
+	}
+
+	scope := fmt.Sprintf("repository:%s:pull", image.Image)
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `image` parameter is used here to construct the `scope` string by accessing `image.Image`. If `image` is `nil` (as discussed in `GhcrRequest`), this will cause a panic.

To improve robustness, should `ChallengeReq` check if `image` is `nil` at the beginning of the function and handle it appropriately (e.g., log an error and return an empty token or an error)?

```suggestion
	if image == nil {
		logError("ChallengeReq called with nil imageInfo for target: %s", target)
		// Optionally, inform the client via HandleError(c, ...) if appropriate, 
		// or ensure the caller handles the empty token gracefully.
		return "" // Return empty token, the subsequent request will likely fail auth
	}
	scope := fmt.Sprintf("repository:%s:pull", image.Image)
```
created_at: 2025-05-21T01:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
 		)
 	}
 }
+
+func initGhcrHTTPClient(cfg *config.Config) {
+	var proTolcols = new(http.Protocols)
+	proTolcols.SetHTTP1(true)
+	proTolcols.SetHTTP2(true)
+	if cfg.Httpc.Mode == "auto" {
+
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+			Protocols:       proTolcols,
+		}
+	} else if cfg.Httpc.Mode == "advanced" {
+		ghcrtr = &http.Transport{
+			MaxIdleConns:        cfg.Httpc.MaxIdleConns,
+			MaxConnsPerHost:     cfg.Httpc.MaxConnsPerHost,
+			MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
+			WriteBufferSize:     32 * 1024, // 32KB
+			ReadBufferSize:      32 * 1024, // 32KB
+			Protocols:           proTolcols,
+		}
+	} else {
+		// 错误的模式
+		logError("unknown httpc mode: %s", cfg.Httpc.Mode)
+		fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
+		logWarning("use Auto to Run HTTP Client")
+		fmt.Println("use Auto to Run HTTP Client")
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+		}
path: proxy/httpc.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

In the case of an unknown `cfg.Httpc.Mode`, the `ghcrtr` transport is initialized but `Protocols: proTolcols` is not set, unlike in the `"auto"` or `"advanced"` modes. This could lead to different HTTP protocol behavior for the GHCR client if an invalid mode is configured.

Would it be better to ensure consistent protocol settings, perhaps by including `Protocols: proTolcols` here as well, or by defaulting to the `"auto"` mode's transport settings?

```go
		ghcrtr = &http.Transport{
			IdleConnTimeout: 30 * time.Second,
			WriteBufferSize: 32 * 1024, // 32KB
			ReadBufferSize:  32 * 1024, // 32KB
			Protocols:       proTolcols, // Add this line for consistency
		}
```
created_at: 2025-05-21T01:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
 		)
 	}
 }
+
+func initGhcrHTTPClient(cfg *config.Config) {
+	var proTolcols = new(http.Protocols)
+	proTolcols.SetHTTP1(true)
+	proTolcols.SetHTTP2(true)
+	if cfg.Httpc.Mode == "auto" {
+
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+			Protocols:       proTolcols,
+		}
+	} else if cfg.Httpc.Mode == "advanced" {
+		ghcrtr = &http.Transport{
+			MaxIdleConns:        cfg.Httpc.MaxIdleConns,
+			MaxConnsPerHost:     cfg.Httpc.MaxConnsPerHost,
+			MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
+			WriteBufferSize:     32 * 1024, // 32KB
+			ReadBufferSize:      32 * 1024, // 32KB
+			Protocols:           proTolcols,
+		}
+	} else {
+		// 错误的模式
+		logError("unknown httpc mode: %s", cfg.Httpc.Mode)
+		fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
+		logWarning("use Auto to Run HTTP Client")
+		fmt.Println("use Auto to Run HTTP Client")
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+		}
+	}
+	if cfg.Outbound.Enabled {
+		initTransport(cfg, ghcrtr)
+	}
+	if cfg.Server.Debug {
+		ghcrclient = httpc.New(
+			httpc.WithTransport(ghcrtr),
+			httpc.WithDumpLog(),
+		)
+	} else {
+		ghcrclient = httpc.New(
+			httpc.WithTransport(ghcrtr),
+		)
+	}
+}
path: proxy/httpc.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The `initGhcrHTTPClient` function shares a lot of common initialization logic with `initHTTPClient` and `initGitHTTPClient`, particularly for setting up the `http.Transport` based on `cfg.Httpc.Mode` and `cfg.Outbound.Enabled`, and for creating the `httpc.Client`.

To improve maintainability and reduce redundancy, have you considered refactoring this common logic into a shared helper function that could be called by all three initialization functions?
created_at: 2025-05-21T01:06:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+package proxy
+
+import (
+	"fmt"
+	"strings"
+)
+
+// BearerAuthParams 用于存放解析出的 Bearer 认证参数
+type BearerAuthParams struct {
+	Realm   string
+	Service string
+	Scope   string
+}
+
+// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
+// 它期望格式为 'Bearer key1="value1",key2="value2",...'
+// 并尝试将已知参数解析到 BearerAuthParams struct 中。
+func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
+	if headerValue == "" {
+		return nil, fmt.Errorf("header value is empty")
+	}
+
+	// 检查 Scheme 是否是 "Bearer"
+	parts := strings.SplitN(headerValue, " ", 2)
+	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+		return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
+	}
+	paramsStr := parts[1]
+
+	paramPairs := strings.Split(paramsStr, ",")
+	tempMap := make(map[string]string)
+
+	for _, pair := range paramPairs {
+		trimmedPair := strings.TrimSpace(pair)
+		keyValue := strings.SplitN(trimmedPair, "=", 2)
+		if len(keyValue) != 2 {
+			logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
+			continue
+		}
+		key := strings.TrimSpace(keyValue[0])
+		value := strings.TrimSpace(keyValue[1])
+		if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
+			value = value[1 : len(value)-1]
+		}
+		tempMap[key] = value
+	}
path: proxy/authparse.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**逗号分割方式对包含逗号的引号值不安全**

`strings.Split(paramsStr, ",")` 会把形如  
`scope="repository:foo/bar,ref:sha256:xxxx"`  
拆断，导致解析结果缺失后半段 scope。建议使用正则或状态机解析，或借助 `github.com/knadh/koanf/parsers` 等现成实现。

示例改进：

```go
scanner := textproto.NewReader(bufio.NewReader(strings.NewReader(paramsStr)))
for {
    kv, err := scanner.ReadMIMEHeader() // 遵循 RFC 7235
    ...
}
```

或在拆分前检测引号计数来决定是否继续拼接。

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In proxy/authparse.go around lines 30 to 46, the current approach using
strings.Split(paramsStr, ",") incorrectly splits parameters when values contain
commas inside quotes, causing incomplete parsing of parameters like scope. To
fix this, replace the simple split with a more robust parsing method that
respects quoted strings, such as using a state machine, a regular expression
that handles quoted commas, or leveraging existing libraries like
github.com/knadh/koanf/parsers. Alternatively, implement logic to count quotes
and only split on commas outside quotes to ensure correct parameter extraction.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T01:07:36Z
author_association: NONE
diff_hunk: @@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 		req.Header.Add(headerKey, headerValue)
 	})
 
-	resp, err = client.Do(req)
+	req.Header.Set("Host", target)
+	token, exist := cache.Get(image.Image)
+	if exist {
+		logDebug("Use Cache Token: %s", token)
+		req.Header.Set("Authorization", "Bearer "+token)
+	}
+
+	resp, err = ghcrclient.Do(req)
 	if err != nil {
 		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
 		return
 	}
 
-	// 错误处理(404)
-	if resp.StatusCode == 404 {
+	// 处理状态码
+	if resp.StatusCode == 401 {
+		// 请求target /v2/路径
+		if string(c.Request.URI().Path()) != "/v2/" {
+			resp.Body.Close()
+			token := ChallengeReq(target, image, ctx, c)
+
+			// 更新kv
+			if token != "" {
+				logDump("Update Cache Token: %s", token)
+				cache.Put(image.Image, token)
+			}
+
+			rb := ghcrclient.NewRequestBuilder(string(method), u)
+			rb.NoDefaultHeaders()
+			rb.SetBody(c.Request.BodyStream())
+			rb.WithContext(ctx)
+
+			req, err = rb.Build()
+			if err != nil {
+				HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+				return
+			}
+
+			c.Request.Header.VisitAll(func(key, value []byte) {
+				headerKey := string(key)
+				headerValue := string(value)
+				req.Header.Add(headerKey, headerValue)
+			})
+
+			req.Header.Set("Host", target)
+			if token != "" {
+				req.Header.Set("Authorization", "Bearer "+token)
+			}
+
+			resp, err = ghcrclient.Do(req)
+			if err != nil {
+				HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+				return
+			}
+		}
+
path: proxy/docker.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**401 重新发起请求时重复代码且可能继续泄漏旧凭据**

在处理 401 时重复构造请求与复制头的代码块，与前面几乎完全一致，存在：

- 维护成本高（两处逻辑需同步修改）  
- 再次复制客户端 `Authorization` 头的问题仍未解决  

可提炼成内部函数 `buildUpstreamRequest()`，并复用；同时按上条评论过滤敏感头。

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In proxy/docker.go around lines 181 to 222, the code handling 401 responses
duplicates request construction and header copying logic, causing maintenance
overhead and risking leaking old Authorization headers. Refactor by extracting
this repeated logic into a helper function named buildUpstreamRequest() that
builds and returns the upstream request. Within this function, ensure to filter
out sensitive headers like Authorization before copying headers from the
original request. Replace the duplicated code with calls to this new function to
improve code reuse and security.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T01:07:36Z
author_association: NONE
diff_hunk: @@ -2,39 +2,132 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
path: proxy/docker.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**全局缓存可能未初始化导致空指针**

`cache` 是包级变量，但只有在显式调用 `InitWeakCache()` 后才会被赋值。  
如果上层（如 `main.go`）忘记调用该函数，`GhcrRequest()` 中的 `cache.Get()` 将触发 panic。

建议做惰性初始化，或在 `init()` 函数里保证一次性创建：

```diff
 var cache *weakcache.Cache[string]

+func getCache() *weakcache.Cache[string] {
+	if cache == nil {
+		cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	}
+	return cache
+}
```

并将所有 `cache.Get/Put` 调用改为 `getCache().Get/Put`。  
这样即使上层忘记调用也不会导致崩溃。

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In proxy/docker.go around lines 24 to 35, the package-level variable cache may
be nil if InitWeakCache() is not called, causing a panic when cache.Get() is
used. To fix this, implement a getCache() function that checks if cache is nil
and initializes it if needed, ensuring lazy initialization. Replace all direct
cache.Get/Put calls with getCache().Get/Put to prevent nil pointer dereference
even if InitWeakCache() is not explicitly called.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T01:07:36Z
author_association: NONE
diff_hunk: @@ -2,39 +2,132 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
+
+/*
 func GhcrRouting(cfg *config.Config) app.HandlerFunc {
 	return func(ctx context.Context, c *app.RequestContext) {
-		if cfg.Docker.Enabled {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		path := ""
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			path = c.Param("filepath")
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, nil)
+
+	}
+}
+*/
+
+func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
+	return func(ctx context.Context, c *app.RequestContext) {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		reqImageUser := c.Param("user")
+		reqImageName := c.Param("repo")
+		reqFilePath := c.Param("filepath")
+
+		path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+		target := ""
+
path: proxy/docker.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**路径拼接可能出现重复 `/`**

`reqFilePath` 在路由规则里通常已包含前导 `/`。  
当前做法直接 `fmt.Sprintf("%s/%s/%s", …)`，容易生成 `user/repo//manifest` 这类双斜杠路径，导致后端 404。

```diff
-	path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+	cleanFilePath := strings.TrimPrefix(reqFilePath, "/")
+	path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, cleanFilePath)
```

同时补充单元测试验证不同输入组合。  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        charToFind := '.'
        reqTarget := c.Param("target")
        reqImageUser := c.Param("user")
        reqImageName := c.Param("repo")
        reqFilePath := c.Param("filepath")

        cleanFilePath := strings.TrimPrefix(reqFilePath, "/")
        path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, cleanFilePath)
        target := ""
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In proxy/docker.go around lines 69 to 77, the path concatenation uses
fmt.Sprintf("%s/%s/%s", ...) directly, which can cause duplicate slashes if
reqFilePath starts with a leading slash, resulting in paths like
"user/repo//manifest" and causing backend 404 errors. To fix this, sanitize
reqFilePath by trimming any leading slashes before concatenation or use a path
join function that handles slashes properly. Additionally, add unit tests
covering various input combinations to verify the path construction handles
leading slashes correctly and avoids duplicate slashes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T01:07:36Z
author_association: NONE
diff_hunk: @@ -2,39 +2,132 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
+
+/*
 func GhcrRouting(cfg *config.Config) app.HandlerFunc {
 	return func(ctx context.Context, c *app.RequestContext) {
-		if cfg.Docker.Enabled {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		path := ""
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			path = c.Param("filepath")
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, nil)
+
+	}
+}
+*/
+
+func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
+	return func(ctx context.Context, c *app.RequestContext) {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		reqImageUser := c.Param("user")
+		reqImageName := c.Param("repo")
+		reqFilePath := c.Param("filepath")
+
+		path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+			reqImageUser = c.Param("target")
+			reqImageName = c.Param("user")
+		}
+		image := &imageInfo{
+			User:  reqImageUser,
+			Repo:  reqImageName,
+			Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, image)
+
+	}
+
+}
+
+func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
+	if cfg.Docker.Enabled {
+		if target != "" {
+			GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target)
+
+		} else {
 			if cfg.Docker.Target == "ghcr" {
path: proxy/docker.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**QueryString 缺少 `?` 前缀**

`c.Request.QueryString()` 返回裸的查询串（如 `tag=latest`），直接拼接到 URL 会丢失 `?`，导致请求被误解析为路径的一部分。

```diff
- GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target)
+ qs := c.Request.QueryString()
+ url := "https://" + target + "/v2/" + path
+ if len(qs) > 0 {
+     url += "?" + string(qs)
+ }
+ GhcrRequest(ctx, c, url, image, cfg, target)
```

同类问题在后续 `else if` 分支也存在，请一并修复。  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
    if cfg.Docker.Enabled {
        if target != "" {
            // prepend “?” only when there is a non-empty query string
            qs := c.Request.QueryString()
            url := "https://" + target + "/v2/" + path
            if len(qs) > 0 {
                url += "?" + string(qs)
            }
            GhcrRequest(ctx, c, url, image, cfg, target)

        } else {
            if cfg.Docker.Target == "ghcr" {
                // …
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In proxy/docker.go around lines 104 to 110, the code concatenates the query
string from c.Request.QueryString() directly to the URL without a '?' prefix,
causing the query parameters to be treated as part of the path. To fix this,
check if the query string is non-empty and prepend a '?' before appending it to
the URL in all relevant places, including the else if branches, ensuring the
query string is correctly formatted in the URL.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T01:07:36Z
author_association: NONE
diff_hunk: @@ -136,3 +279,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 	c.SetBodyStream(bodyReader, -1)
 
 }
+
+type AuthToken struct {
+	Token string `json:"token"`
+}
+
+func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
+	var resp401 *http.Response
+	var req401 *http.Request
+	var err error
+
+	rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
+	rb401.NoDefaultHeaders()
+	rb401.WithContext(ctx)
+	rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
+	req401, err = rb401.Build()
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+		return
+	}
+	req401.Header.Set("Host", target)
+
+	resp401, err = ghcrclient.Do(req401)
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+		return
+	}
+	defer resp401.Body.Close()
+	bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
+	if err != nil {
+		logError("Failed to parse Www-Authenticate header: %v", err)
+		return
+	}
+
+	scope := fmt.Sprintf("repository:%s:pull", image.Image)
+
+	getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
+		NoDefaultHeaders().
+		WithContext(ctx).
+		AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ").
+		SetHeader("Host", bearer.Service).
+		AddQueryParam("service", bearer.Service).
+		AddQueryParam("scope", scope)
+
+	getAuthReq, err := getAuthRB.Build()
+	if err != nil {
+		logError("Failed to create request: %v", err)
+		return
+	}
+
+	authResp, err := ghcrclient.Do(getAuthReq)
+	if err != nil {
+		logError("Failed to send request: %v", err)
+		return
+	}
+
+	defer authResp.Body.Close()
+
+	bodyBytes, err := io.ReadAll(authResp.Body)
+	if err != nil {
+		logError("Failed to read auth response body: %v", err)
+		return
+	}
+
+	// 解码json
+	var authToken AuthToken
+	err = json.Unmarshal(bodyBytes, &authToken)
+	if err != nil {
+		logError("Failed to decode auth response body: %v", err)
+		return
+	}
+	token = authToken.Token
+
+	return token
+
+}
path: proxy/docker.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**请考虑添加单元测试覆盖 ChallengeReq 流程**

新增的 `ChallengeReq` 函数实现了重要的认证功能，建议添加单元测试以确保：

1. Bearer 认证头解析正确
2. 认证请求参数构造正确
3. 异常情况（网络错误、解析错误等）的处理符合预期
4. Token 缓存的储存和获取正常工作

这将提高代码质量并防止未来修改引入回归问题。

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In proxy/docker.go around lines 287 to 356, the ChallengeReq function lacks unit
test coverage. Add unit tests to verify correct parsing of the Bearer
authentication header, proper construction of the authentication request
parameters, and correct handling of error cases such as network failures and
JSON parsing errors. Also, include tests to ensure token caching mechanisms
store and retrieve tokens as expected. This will improve code reliability and
prevent regressions.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T04:11:33Z
author_association: NONE
diff_hunk: @@ -2,39 +2,131 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
+
+/*
 func GhcrRouting(cfg *config.Config) app.HandlerFunc {
 	return func(ctx context.Context, c *app.RequestContext) {
-		if cfg.Docker.Enabled {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		path := ""
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			path = c.Param("filepath")
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, nil)
+
+	}
+}
+*/
+
+func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
+	return func(ctx context.Context, c *app.RequestContext) {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		reqImageUser := c.Param("user")
+		reqImageName := c.Param("repo")
+		reqFilePath := c.Param("filepath")
+
+		path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+			reqImageUser = c.Param("target")
+			reqImageName = c.Param("user")
+		}
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

In the `else` block of `GhcrWithImageRouting` (lines 87-91), if `reqTarget` does not contain '.', the code assumes a different URL structure. It sets `path = string(c.Request.RequestURI())`, and then reassigns `reqImageUser = c.Param("target")` and `reqImageName = c.Param("user")`. 

This seems problematic because `c.Param("target")` was already assigned to `reqTarget` and `c.Param("user")` to `reqImageUser`. If the route is `/v2/:target/:user/:repo/*filepath`, and `reqTarget` (the first segment after `/v2/`) doesn't contain a '.', it implies the first segment is not a domain-like target. 

In this `else` case, `path` becomes the full request URI (e.g., `/v2/segment1/segment2/segment3/manifests/latest`). Then `reqImageUser` becomes `segment1` and `reqImageName` becomes `segment2`. This means the `imageInfo` will be constructed based on these segments. 

Is this the intended fallback behavior? It seems like it might lead to incorrect `imageInfo` if the URL structure doesn't match the expected pattern when `reqTarget` lacks a `.`.
Could you clarify the expected URL patterns for this `else` block and ensure the parameter parsing aligns with it? It might be safer to return an error or a specific response if the URL structure is ambiguous or doesn't fit a recognized pattern.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,3 +279,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 	c.SetBodyStream(bodyReader, -1)
 
 }
+
+type AuthToken struct {
+	Token string `json:"token"`
+}
+
+func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
+	var resp401 *http.Response
+	var req401 *http.Request
+	var err error
+
+	rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
+	rb401.NoDefaultHeaders()
+	rb401.WithContext(ctx)
+	rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
+	req401, err = rb401.Build()
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+		return
+	}
+	req401.Header.Set("Host", target)
+
+	resp401, err = ghcrclient.Do(req401)
+	if err != nil {
+		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+		return
+	}
+	defer resp401.Body.Close()
+	bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
+	if err != nil {
+		logError("Failed to parse Www-Authenticate header: %v", err)
+		return
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

In `ChallengeReq`, if `parseBearerWWWAuthenticateHeader` returns an error, the function logs the error and then `return`s. Since the function has a named return value `token string`, this will return the zero value for `token` (an empty string) without explicitly assigning it. 

This pattern of logging an error and returning a zero value is repeated for other errors (e.g., request creation, `ghcrclient.Do`, `io.ReadAll`, `json.Unmarshal`).

The caller, `GhcrRequest` (line 191), receives this empty token. If `token` is empty, `cache.Put` is not called (line 194), and the subsequent request (line 218) will not have an `Authorization` header if the token was empty. This might lead to repeated 401s if the token acquisition consistently fails.

Should `ChallengeReq` more explicitly signal failure to its caller, perhaps by returning an `error` alongside the token? This would allow `GhcrRequest` to handle token acquisition failures more robustly, e.g., by not retrying or by returning a specific error to the client. Simply returning an empty token makes it hard for the caller to distinguish between a successful acquisition of an empty token (unlikely) and a failed acquisition.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+package proxy
+
+import (
+	"fmt"
+	"strings"
+)
+
+// BearerAuthParams 用于存放解析出的 Bearer 认证参数
+type BearerAuthParams struct {
+	Realm   string
+	Service string
+	Scope   string
+}
+
+// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
+// 它期望格式为 'Bearer key1="value1",key2="value2",...'
+// 并尝试将已知参数解析到 BearerAuthParams struct 中。
+func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
+	if headerValue == "" {
+		return nil, fmt.Errorf("header value is empty")
+	}
+
+	// 检查 Scheme 是否是 "Bearer"
+	parts := strings.SplitN(headerValue, " ", 2)
+	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+		return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
path: proxy/authparse.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The error message `invalid or non-bearer header format: got '%s'` includes the full `headerValue`. If the `headerValue` could potentially contain sensitive information (though less likely for Www-Authenticate itself, but good practice), it might be better to either not include the full value or to truncate it in logs/errors.

Consider if it's necessary to expose the entire header value in the error. For debugging, it's useful, but for production logs, it might be too verbose or a slight information disclosure risk.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,80 +49,91 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
 		apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
 			SmartGitStatusHandler(cfg, c, ctx)
 		})
-
+		apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) {
+			shellNestStatusHandler(cfg, c, ctx)
+		})
+		apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) {
+			ociProxyStatusHandler(cfg, c, ctx)
+		})
 	}
 	logInfo("API router Init success")
 }
 
 func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
 	sizeLimit := cfg.Server.SizeLimit
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"MaxResponseBodySize": sizeLimit,
 	}))
 }
 
 func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Whitelist": cfg.Whitelist.Enabled,
 	}))
 }
 
 func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Blacklist": cfg.Blacklist.Enabled,
 	}))
 }
 
 func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Cors": cfg.Server.Cors,
 	}))
 }
 
 func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Status": "OK",
 	}))
 }
 
 func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Version": version,
 	}))
 }
 
 func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"RateLimit": cfg.RateLimit.Enabled,
 	}))
 }
 
 func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"RatePerMinute": cfg.RateLimit.RatePerMinute,
 	}))
 }
 
 func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
path: api/api.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

It seems like the logging calls `logInfo("%s %s %s %s %s", ...)` were removed from several handlers (`SizeLimitHandler`, `WhiteListStatusHandler`, `BlackListStatusHandler`, `CorsStatusHandler`, `HealthcheckHandler`, `VersionHandler`, `RateLimitStatusHandler`, `RateLimitLimitHandler`, `SmartGitStatusHandler`).

Was this removal intentional? While excessive logging can be noisy, these logs provided basic access logging for these API endpoints, including client IP, method, path, and user agent. If this information is still valuable for monitoring or debugging, perhaps a more structured or configurable logging approach could be considered for API requests, or these specific logs could be retained if their removal wasn't intended to be a general change.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+package proxy
+
+import (
+	"fmt"
+	"strings"
+)
+
+// BearerAuthParams 用于存放解析出的 Bearer 认证参数
+type BearerAuthParams struct {
+	Realm   string
+	Service string
+	Scope   string
+}
+
+// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
+// 它期望格式为 'Bearer key1="value1",key2="value2",...'
+// 并尝试将已知参数解析到 BearerAuthParams struct 中。
+func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
+	if headerValue == "" {
+		return nil, fmt.Errorf("header value is empty")
+	}
+
+	// 检查 Scheme 是否是 "Bearer"
+	parts := strings.SplitN(headerValue, " ", 2)
+	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+		return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
+	}
+	paramsStr := parts[1]
+
+	paramPairs := strings.Split(paramsStr, ",")
+	tempMap := make(map[string]string)
+
+	for _, pair := range paramPairs {
+		trimmedPair := strings.TrimSpace(pair)
+		keyValue := strings.SplitN(trimmedPair, "=", 2)
+		if len(keyValue) != 2 {
+			logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
path: proxy/authparse.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The function logs a warning `logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)` for malformed parameters but continues processing other parameters. This is a reasonable approach for robustness.

However, should the function also return an error or a specific status indicating that parts of the header were unparseable, even if it successfully parsed other parts? This might depend on how strictly the caller needs to adhere to the header format. If any malformed part makes the entire header potentially invalid for the caller's purpose, returning an error might be more appropriate.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,39 +2,131 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The `InitWeakCache` function initializes a global `cache` variable. This pattern can sometimes make testing harder as tests might interfere with each other's cache state if not carefully managed. 

Would it be more flexible to have `GhcrWithImageRouting` (or a struct that holds the routing logic) take the cache as a dependency (e.g., passed in during initialization)? This would improve testability and make dependencies more explicit. However, given `wcache` is already a global in `main.go`, this might be a larger refactoring.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -530,8 +512,9 @@ func main() {
 			http.ListenAndServe("localhost:6060", nil)
 		}()
 	}
-
-	r.Spin()
+	if wcache != nil {
+		defer wcache.StopCleanup()
+	}
path: main.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The `defer wcache.StopCleanup()` is placed here, while `r.Spin()` is called later (line 528). If `r.Spin()` blocks indefinitely (as it typically does for a server), `StopCleanup` might only be called when the program is explicitly terminated, not necessarily on a graceful shutdown initiated elsewhere.

Is this the intended behavior? If `wcache.StopCleanup()` should be called as part of a more controlled shutdown sequence (e.g., triggered by a signal), its placement and invocation might need to be coordinated with how `r.Spin()` is managed or how shutdown signals are handled.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,39 +2,131 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
+
+/*
 func GhcrRouting(cfg *config.Config) app.HandlerFunc {
 	return func(ctx context.Context, c *app.RequestContext) {
-		if cfg.Docker.Enabled {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		path := ""
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			path = c.Param("filepath")
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, nil)
+
+	}
+}
+*/
+
+func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
+	return func(ctx context.Context, c *app.RequestContext) {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		reqImageUser := c.Param("user")
+		reqImageName := c.Param("repo")
+		reqFilePath := c.Param("filepath")
+
+		path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+			reqImageUser = c.Param("target")
+			reqImageName = c.Param("user")
+		}
+		image := &imageInfo{
+			User:  reqImageUser,
+			Repo:  reqImageName,
+			Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, image)
+
+	}
+
+}
+
+func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) {
+	if cfg.Docker.Enabled {
+		if target != "" {
+			GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target)
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The URL construction `"https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString())` appends the query string. If `path` already contains query parameters (e.g., if `reqFilePath` in `GhcrWithImageRouting` somehow included them, or if `string(c.Request.RequestURI())` in the fallback case did), this could lead to a malformed URL with two `?`.

It might be safer to parse the `path`, reconstruct the URL, and then merge query parameters carefully. Alternatively, ensure that `path` will never contain its own query string.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,14 +164,68 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 		req.Header.Add(headerKey, headerValue)
 	})
 
-	resp, err = client.Do(req)
+	req.Header.Set("Host", target)
+	if image != nil {
+		token, exist := cache.Get(image.Image)
+		if exist {
+			logDebug("Use Cache Token: %s", token)
+			req.Header.Set("Authorization", "Bearer "+token)
+		}
+	}
+
+	resp, err = ghcrclient.Do(req)
 	if err != nil {
 		HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
 		return
 	}
 
-	// 错误处理(404)
-	if resp.StatusCode == 404 {
+	// 处理状态码
+	if resp.StatusCode == 401 {
+		// 请求target /v2/路径
+		if string(c.Request.URI().Path()) != "/v2/" {
+			resp.Body.Close()
+			if image == nil {
+				ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
+				return
+			}
+			token := ChallengeReq(target, image, ctx, c)
+
+			// 更新kv
+			if token != "" {
+				logDump("Update Cache Token: %s", token)
+				cache.Put(image.Image, token)
+			}
+
+			rb := ghcrclient.NewRequestBuilder(string(method), u)
+			rb.NoDefaultHeaders()
+			rb.SetBody(c.Request.BodyStream())
+			rb.WithContext(ctx)
+
+			req, err = rb.Build()
+			if err != nil {
+				HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
+				return
+			}
+
+			c.Request.Header.VisitAll(func(key, value []byte) {
+				headerKey := string(key)
+				headerValue := string(value)
+				req.Header.Add(headerKey, headerValue)
+			})
+
+			req.Header.Set("Host", target)
+			if token != "" {
+				req.Header.Set("Authorization", "Bearer "+token)
+			}
+
+			resp, err = ghcrclient.Do(req)
+			if err != nil {
+				HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
+				return
+			}
+		}
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

In the 401 handling block, there's a condition `if string(c.Request.URI().Path()) != "/v2/"`. This prevents token challenge/retry for requests directly to `/v2/`. 

However, if `image == nil` (line 187), it returns an error. This implies that `image` is required for the token challenge. What if a request that *should* have an image (e.g. `/v2/library/alpine/manifests/latest`) somehow results in `image == nil` here? The current logic would prevent the challenge. 

Also, the retry logic (lines 199-225) duplicates the request creation and header copying from the initial request (lines 150-165). Could this be refactored into a helper function to reduce duplication and improve maintainability?
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,3 +279,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf
 	c.SetBodyStream(bodyReader, -1)
 
 }
+
+type AuthToken struct {
+	Token string `json:"token"`
+}
+
+func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) {
+	var resp401 *http.Response
+	var req401 *http.Request
+	var err error
+
+	rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
+	rb401.NoDefaultHeaders()
+	rb401.WithContext(ctx)
+	rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
path: proxy/docker.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The User-Agent string `"docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 "` is hardcoded. While this might be necessary to mimic a real Docker client for compatibility with certain registries, it could become outdated.

Is it possible to make this configurable or dynamically determine parts of it if necessary? If it must be hardcoded, a comment explaining why this specific string is used would be beneficial.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -289,7 +296,7 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
 		staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
 	})
 	r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
-		staticServer := http.FileServer(http.FS(pages))
path: main.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The `FileServer` for `/favicon.ico` has been changed from `http.FS(pages)` to `http.FS(assets)`. Could you clarify the reasoning for this change? 

Typically, `favicon.ico` is considered an asset. If `pages` was intended to serve only HTML/template files and `assets` for static resources like icons, images, CSS, JS, then this change makes sense. However, it's worth ensuring that the `assets` `fs.FS` correctly points to the directory containing `favicon.ico` after the `fs.Sub` operation in `loadEmbeddedPages`.

```suggestion
		staticServer := http.FileServer(http.FS(assets)) // Ensure 'assets' correctly contains favicon.ico
```
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) {
 		)
 	}
 }
+
+func initGhcrHTTPClient(cfg *config.Config) {
+	var proTolcols = new(http.Protocols)
+	proTolcols.SetHTTP1(true)
+	proTolcols.SetHTTP2(true)
+	if cfg.Httpc.Mode == "auto" {
+
+		ghcrtr = &http.Transport{
+			IdleConnTimeout: 30 * time.Second,
+			WriteBufferSize: 32 * 1024, // 32KB
+			ReadBufferSize:  32 * 1024, // 32KB
+			Protocols:       proTolcols,
+		}
+	} else if cfg.Httpc.Mode == "advanced" {
+		ghcrtr = &http.Transport{
+			MaxIdleConns:        cfg.Httpc.MaxIdleConns,
+			MaxConnsPerHost:     cfg.Httpc.MaxConnsPerHost,
+			MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
+			WriteBufferSize:     32 * 1024, // 32KB
+			ReadBufferSize:      32 * 1024, // 32KB
+			Protocols:           proTolcols,
+		}
+	} else {
+		// 错误的模式
+		logError("unknown httpc mode: %s", cfg.Httpc.Mode)
+		fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
+		logWarning("use Auto to Run HTTP Client")
+		fmt.Println("use Auto to Run HTTP Client")
path: proxy/httpc.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

When an unknown `cfg.Httpc.Mode` is encountered in `initGhcrHTTPClient` (and similarly in `initHTTPClient` and `initGitHTTPClient`), the code logs an error, prints to `fmt.Println`, logs a warning, and prints another message to `fmt.Println`, then falls back to the "auto" mode's transport settings.

While falling back to a default is good for resilience, printing directly to `stdout` using `fmt.Println` bypasses the configured logger and might not be desirable in production environments where logs are expected to go to specific files or systems. 

Could the `fmt.Println` calls be removed, relying solely on the `hlog` or your custom `logger` for all output? If an invalid config is a critical issue, perhaps the program should exit or the error should be propagated to `main` to handle.
created_at: 2025-05-21T04:15:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+package weakcache
+
+import (
+	"container/list"
+	"sync"
+	"time"
+	"weak" // Go 1.24 引入的 weak 包
+)
+
+// DefaultExpiration 默认过期时间，这里设置为 15 分钟。
+// 这是一个导出的常量，方便用户使用包时引用默认值。
+const DefaultExpiration = 5 * time.Minute
path: weakcache/weakcache.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The comment for `DefaultExpiration` says "默认过期时间，这里设置为 15 分钟。" (15 minutes), but the value is `5 * time.Minute`. This discrepancy should be corrected. Which one is intended?
created_at: 2025-05-21T04:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+package weakcache
+
+import (
+	"container/list"
+	"sync"
+	"time"
+	"weak" // Go 1.24 引入的 weak 包
+)
+
+// DefaultExpiration 默认过期时间，这里设置为 15 分钟。
+// 这是一个导出的常量，方便用户使用包时引用默认值。
+const DefaultExpiration = 5 * time.Minute
+
+// cleanupInterval 是后台清理 Go routine 的扫描间隔，这里设置为 5 分钟。
+// 这是一个内部常量，不导出。
+const cleanupInterval = 2 * time.Minute
+
+// cacheEntry 缓存项的内部结构。不导出。
+type cacheEntry[T any] struct {
+	Value      T
+	Expiration time.Time
+	key        string // 存储key，方便在list.Element中引用
+}
+
+// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
+// 这是一个导出的类型。
+type Cache[T any] struct {
+	mu sync.RWMutex
+
+	// 修正：缓存存储：key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
+	// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
+	data map[string]weak.Pointer[cacheEntry[T]]
+
+	// FIFO 链表：存储 key 的 list.Element
+	// 链表头部是最近放入的，尾部是最早放入的（最老的）
+	fifoList *list.List
+	// FIFO 元素的映射：key -> *list.Element
+	fifoMap map[string]*list.Element
+
+	defaultExpiration time.Duration
+	maxSize           int // 缓存最大容量，0 表示无限制
+
+	stopCleanup chan struct{}
+	wg          sync.WaitGroup // 用于等待清理 Go routine 退出
+}
+
+// NewCache 创建一个新的缓存实例。
+// expiration: 新添加项的默认过期时间。如果为 0，则使用 DefaultExpiration。
+// maxSize: 缓存的最大容量，0 表示无限制。当达到上限时，采用 FIFO 策略淘汰。
+// 这是一个导出的构造函数。
+func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
+	if expiration <= 0 {
+		expiration = DefaultExpiration
+	}
+
+	c := &Cache[T]{
+		// 修正：初始化 map，值类型已修正
+		data:              make(map[string]weak.Pointer[cacheEntry[T]]),
+		fifoList:          list.New(),
+		fifoMap:           make(map[string]*list.Element),
+		defaultExpiration: expiration,
+		maxSize:           maxSize,
+		stopCleanup:       make(chan struct{}),
+	}
+	// 启动后台清理 Go routine
+	c.wg.Add(1)
+	go c.cleanupLoop()
+	return c
+}
+
+// Put 将值放入缓存。如果 key 已存在，会更新其值和过期时间。
+// 这是导出的方法。
+func (c *Cache[T]) Put(key string, value T) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	now := time.Now()
+	expiration := now.Add(c.defaultExpiration)
+
+	// 如果 key 已经存在，更新其值和过期时间。
+	// 在 FIFO 策略中， Put 更新不改变其在链表中的位置，除非旧的 entry 已经被 GC。
+	if elem, ok := c.fifoMap[key]; ok {
+		// 从 data map 中获取弱引用，wp 的类型现在是 weak.Pointer[cacheEntry[T]]
+		if wp, dataOk := c.data[key]; dataOk {
+			// wp.Value() 返回 *cacheEntry[T]， entry 的类型现在是 *cacheEntry[T]
+			entry := wp.Value()
+			if entry != nil {
+				// 旧的 cacheEntry 仍在内存中，直接更新
+				entry.Value = value
+				entry.Expiration = expiration
+				// 在严格 FIFO 中，更新不移动位置
+				return
+			}
+			// 如果 weak.Pointer.Value() 为 nil，说明之前的 cacheEntry 已经被 GC 了
+			// 此时需要创建一个新的 entry，并将其从旧位置移除，再重新添加
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		} else {
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		}
path: weakcache/weakcache.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

In the `Put` method, if an existing `elem` is found in `fifoMap`:
1. If `wp.Value()` (the weak pointer to `cacheEntry`) is `nil` (meaning the entry was GC'd), or if the key is not in `c.data` (though this case seems less likely if it's in `fifoMap`), the old element is removed from `fifoList` and `fifoMap`.
2. Then, a new entry is created and added.

This logic seems correct for handling GC'd entries. However, the comment on line 81 states: "In FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC." This is true if the entry is *not* GC'd (lines 88-92). But if it *is* GC'd, it's removed and re-added to the front (as per line 118 `c.fifoList.PushFront(key)`).

This behavior is a mix of update-in-place (for non-GC'd) and remove-then-add-to-front (for GC'd). Is this the intended behavior for a FIFO cache? Typically, a strict FIFO would mean new items go to one end and evictions happen from the other, with updates not changing an item's position relative to eviction order. If an item is updated, it might refresh its TTL but not its FIFO position.

If the goal is more LRU-like behavior on update (move to front), then non-GC'd updates should also move the element to the front of `fifoList`. If it's strict FIFO for eviction regardless of updates or GC status, then even after GC, the new entry should ideally retain its original FIFO slot or be treated as a completely new item that might displace an older one if `maxSize` is hit. The current behavior seems reasonable but the interplay with FIFO eviction upon update (especially after GC) could be clarified or reconsidered if a stricter policy is desired.
created_at: 2025-05-21T04:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+package weakcache
+
+import (
+	"container/list"
+	"sync"
+	"time"
+	"weak" // Go 1.24 引入的 weak 包
+)
+
+// DefaultExpiration 默认过期时间，这里设置为 15 分钟。
+// 这是一个导出的常量，方便用户使用包时引用默认值。
+const DefaultExpiration = 5 * time.Minute
+
+// cleanupInterval 是后台清理 Go routine 的扫描间隔，这里设置为 5 分钟。
+// 这是一个内部常量，不导出。
+const cleanupInterval = 2 * time.Minute
+
+// cacheEntry 缓存项的内部结构。不导出。
+type cacheEntry[T any] struct {
+	Value      T
+	Expiration time.Time
+	key        string // 存储key，方便在list.Element中引用
+}
+
+// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
+// 这是一个导出的类型。
+type Cache[T any] struct {
+	mu sync.RWMutex
+
+	// 修正：缓存存储：key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
+	// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
+	data map[string]weak.Pointer[cacheEntry[T]]
+
+	// FIFO 链表：存储 key 的 list.Element
+	// 链表头部是最近放入的，尾部是最早放入的（最老的）
+	fifoList *list.List
+	// FIFO 元素的映射：key -> *list.Element
+	fifoMap map[string]*list.Element
+
+	defaultExpiration time.Duration
+	maxSize           int // 缓存最大容量，0 表示无限制
+
+	stopCleanup chan struct{}
+	wg          sync.WaitGroup // 用于等待清理 Go routine 退出
+}
+
+// NewCache 创建一个新的缓存实例。
+// expiration: 新添加项的默认过期时间。如果为 0，则使用 DefaultExpiration。
+// maxSize: 缓存的最大容量，0 表示无限制。当达到上限时，采用 FIFO 策略淘汰。
+// 这是一个导出的构造函数。
+func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
+	if expiration <= 0 {
+		expiration = DefaultExpiration
+	}
+
+	c := &Cache[T]{
+		// 修正：初始化 map，值类型已修正
+		data:              make(map[string]weak.Pointer[cacheEntry[T]]),
+		fifoList:          list.New(),
+		fifoMap:           make(map[string]*list.Element),
+		defaultExpiration: expiration,
+		maxSize:           maxSize,
+		stopCleanup:       make(chan struct{}),
+	}
+	// 启动后台清理 Go routine
+	c.wg.Add(1)
+	go c.cleanupLoop()
+	return c
+}
+
+// Put 将值放入缓存。如果 key 已存在，会更新其值和过期时间。
+// 这是导出的方法。
+func (c *Cache[T]) Put(key string, value T) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	now := time.Now()
+	expiration := now.Add(c.defaultExpiration)
+
+	// 如果 key 已经存在，更新其值和过期时间。
+	// 在 FIFO 策略中， Put 更新不改变其在链表中的位置，除非旧的 entry 已经被 GC。
+	if elem, ok := c.fifoMap[key]; ok {
+		// 从 data map 中获取弱引用，wp 的类型现在是 weak.Pointer[cacheEntry[T]]
+		if wp, dataOk := c.data[key]; dataOk {
+			// wp.Value() 返回 *cacheEntry[T]， entry 的类型现在是 *cacheEntry[T]
+			entry := wp.Value()
+			if entry != nil {
+				// 旧的 cacheEntry 仍在内存中，直接更新
+				entry.Value = value
+				entry.Expiration = expiration
+				// 在严格 FIFO 中，更新不移动位置
+				return
+			}
+			// 如果 weak.Pointer.Value() 为 nil，说明之前的 cacheEntry 已经被 GC 了
+			// 此时需要创建一个新的 entry，并将其从旧位置移除，再重新添加
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		} else {
+			c.fifoList.Remove(elem)
+			delete(c.fifoMap, key)
+		}
+	}
+
+	// 新建缓存项 (注意这里是结构体值，而不是指针)
+	// weak.Make 接收的是指针 *T
+	entry := &cacheEntry[T]{ // 创建结构体指针
+		Value:      value,
+		Expiration: expiration,
+		key:        key, // 存储 key
+	}
+
+	// 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map
+	// weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]]，类型匹配 data map 的值类型
+	c.data[key] = weak.Make(entry)
+
+	// 添加到 FIFO 链表头部 (最近放入/更新的在头部)
+	// PushFront 返回新的 list.Element
+	c.fifoMap[key] = c.fifoList.PushFront(key)
+
+	// 检查大小上限并进行淘汰 (淘汰尾部的最老项)
+	c.evictIfNeeded()
+}
+
+// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。
+// 这是导出的方法。
+func (c *Cache[T]) Get(key string) (T, bool) {
+	c.mu.RLock() // 先读锁
+	// 从 data map 中获取弱引用，wp 的类型现在是 weak.Pointer[cacheEntry[T]]
+	wp, ok := c.data[key]
+	c.mu.RUnlock() // 立即释放读锁，如果需要写操作（removeEntry）可以获得锁
+
+	var zero T // 零值
+
+	if !ok {
+		return zero, false
+	}
+
+	// 尝试获取实际的 cacheEntry 指针
+	// wp.Value() 返回 *cacheEntry[T]， entry 的类型现在是 *cacheEntry[T]
+	entry := wp.Value()
+
+	if entry == nil {
+		// 对象已被GC回收，需要清理此弱引用
+		c.removeEntry(key) // 内部会加写锁
path: weakcache/weakcache.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

In the `Get` method, if `entry == nil` (object GC'd) or if `time.Now().After(entry.Expiration)` (expired), `c.removeEntry(key)` is called. This `removeEntry` acquires a `c.mu.Lock()`.

This means `Get` starts with `c.mu.RLock()`, releases it, and then potentially re-acquires `c.mu.Lock()` via `removeEntry`. This lock transitioning is generally fine.

However, consider the case where multiple goroutines call `Get` for the same GC'd or expired key simultaneously. They might all pass the initial RLock/Unlock, find the entry is bad, and then queue up to acquire the write lock in `removeEntry`. Only the first one will actually perform the removal; subsequent ones will find the key already deleted from `c.data` and `c.fifoMap` when their `removeEntry` call executes. This is not a correctness issue as `delete` on a non-existent key is a no-op, but it's a small point of potential contention.

This is a common pattern in cache implementations and often acceptable. An alternative, more complex approach might involve a single goroutine responsible for all removals, signaled by `Get`/`cleanupLoop`, but that would add significant complexity.
created_at: 2025-05-21T04:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,39 +2,131 @@ package proxy
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+
 	"ghproxy/config"
+	"ghproxy/weakcache"
+	"io"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
 	"github.com/cloudwego/hertz/pkg/app"
 )
 
+var (
+	dockerhubTarget = "registry-1.docker.io"
+	ghcrTarget      = "ghcr.io"
+)
+
+var cache *weakcache.Cache[string]
+
+type imageInfo struct {
+	User  string
+	Repo  string
+	Image string
+}
+
+func InitWeakCache() *weakcache.Cache[string] {
+	cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
+	return cache
+}
+
+/*
 func GhcrRouting(cfg *config.Config) app.HandlerFunc {
 	return func(ctx context.Context, c *app.RequestContext) {
-		if cfg.Docker.Enabled {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		path := ""
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			path = c.Param("filepath")
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+		}
+
+		GhcrToTarget(ctx, c, cfg, target, path, nil)
+
+	}
+}
+*/
+
+func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc {
+	return func(ctx context.Context, c *app.RequestContext) {
+
+		charToFind := '.'
+		reqTarget := c.Param("target")
+		reqImageUser := c.Param("user")
+		reqImageName := c.Param("repo")
+		reqFilePath := c.Param("filepath")
+
+		path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
+		target := ""
+
+		if strings.ContainsRune(reqTarget, charToFind) {
+
+			if reqTarget == "docker.io" {
+				target = dockerhubTarget
+			} else if reqTarget == "ghcr.io" {
+				target = ghcrTarget
+			} else {
+				target = reqTarget
+			}
+		} else {
+			path = string(c.Request.RequestURI())
+			reqImageUser = c.Param("target")
+			reqImageName = c.Param("user")
+		}
path: proxy/docker.go
user: login: WJQSERVER
body: @gemini-code-assist 这是符合预期的
created_at: 2025-05-21T04:22:24Z
author_association: MEMBER
diff_hunk: @@ -49,80 +49,91 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
 		apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
 			SmartGitStatusHandler(cfg, c, ctx)
 		})
-
+		apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) {
+			shellNestStatusHandler(cfg, c, ctx)
+		})
+		apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) {
+			ociProxyStatusHandler(cfg, c, ctx)
+		})
 	}
 	logInfo("API router Init success")
 }
 
 func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
 	sizeLimit := cfg.Server.SizeLimit
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"MaxResponseBodySize": sizeLimit,
 	}))
 }
 
 func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Whitelist": cfg.Whitelist.Enabled,
 	}))
 }
 
 func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Blacklist": cfg.Blacklist.Enabled,
 	}))
 }
 
 func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Cors": cfg.Server.Cors,
 	}))
 }
 
 func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Status": "OK",
 	}))
 }
 
 func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"Version": version,
 	}))
 }
 
 func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"RateLimit": cfg.RateLimit.Enabled,
 	}))
 }
 
 func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
 	c.Response.Header.Set("Content-Type", "application/json")
 	c.JSON(200, (map[string]interface{}{
 		"RatePerMinute": cfg.RateLimit.RatePerMinute,
 	}))
 }
 
 func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
-	logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
path: api/api.go
user: login: WJQSERVER
body: 符合预期, 中间件日志记录足够了
created_at: 2025-05-21T04:24:40Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-21T01:04:18Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

本次变更引入了 Docker 镜像代理的多 target 支持与短时 token 缓存机制（weakcache），并对主程序和 proxy 相关逻辑进行了相应调整。新增 weakcache 组件用于缓存 Docker token，调整了路由与鉴权流程，并补充了相关文档和版本号更新。

## Changes

| 文件/路径分组                       | 变更摘要                                                                                                  |
|--------------------------------------|---------------------------------------------------------------------------------------------------------|
| CHANGELOG.md, DEV-VERSION, VERSION   | 新增 v3.4.0 正式版本和两个预发布版本说明，警告勿用于生产环境，列出多 target Docker proxy 支持、weakcache 机制、新主题和新 API 端点；DEV-VERSION 和 VERSION 文件版本号更新。 |
| main.go                             | 新增全局 weakcache 变量，初始化及清理逻辑，扩展嵌入页面支持新主题 "hub"，调整 Docker 路由，完善 `/v2/` 相关处理，优化 defer 调用顺序。     |
| proxy/authparse.go                   | 新增 Bearer 认证头解析逻辑，定义 BearerAuthParams 结构体与解析函数。                                       |
| proxy/docker.go                      | 新增 Docker token 缓存机制与 imageInfo 结构体，完善多 target 路由与 token 获取流程，增强 401 处理及 Host 头管理，新增 ChallengeReq 函数处理鉴权挑战。|
| proxy/httpc.go                       | 新增 GHCR 专用 HTTP 客户端与传输层初始化逻辑，完善 InitReq 初始化流程。                                    |
| proxy/match.go                       | 简化路径前缀处理逻辑，移除条件判断，始终去除首个斜杠。                                                    |
| proxy/utils.go                       | 简化 listCheck 变量声明，逻辑不变。                                                                      |
| weakcache/weakcache.go               | 新增 weakcache 包，实现基于弱引用的通用缓存，支持过期、FIFO 淘汰与并发安全，包含后台清理协程和停止清理方法。      |
| api/api.go                          | 新增 `/shell_nest/status` 和 `/oci_proxy/status` 两个状态查询接口，移除所有接口的内部日志打印。             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Server(main.go)
    participant Proxy
    participant WeakCache
    participant Registry

    Client->>Server(main.go): 请求 /v2/:target/:user/:repo/*filepath
    Server(main.go)->>Proxy: 解析参数，调用 GhcrWithImageRouting
    Proxy->>WeakCache: 查询 token
    alt token 命中
        Proxy->>Registry: 携带 token 请求镜像资源
    else token 未命中或过期
        Proxy->>Registry: 请求 /v2/ 获取 Www-Authenticate
        Registry-->>Proxy: 返回 401 + Www-Authenticate
        Proxy->>Proxy: 解析 Www-Authenticate，获取 realm/service/scope
        Proxy->>Registry: 请求 token
        Registry-->>Proxy: 返回 token
        Proxy->>WeakCache: 缓存 token
        Proxy->>Registry: 携带新 token 重试请求
    end
    Registry-->>Proxy: 返回镜像资源
    Proxy-->>Server(main.go): 返回响应
    Server(main.go)-->>Client: 返回响应
```

## Possibly related PRs

- WJQSERVER-STUDIO/ghproxy#95: 该 PR 初步实现了 Docker 代理支持，当前 PR 在此基础上扩展了多 target 支持和 token 缓存机制，二者在 proxy/docker.go 中有紧密关联。

## Suggested labels

`改进`

## Suggested reviewers

- satomitouka

## Poem

> 兔子写代码，耳朵竖又高，  
> 镜像代理升级啦，multi-target 护航跑。  
> token 缓存弱引用，短命不怕老，  
> 路由清晰又安全，  
> 版本号跃新潮。  
> 🐇💻✨
"""

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1afb3521942a952b57291086889b2d0db2af4017 and 4598257faa9ce5a0b043dc0626a00291585a9734.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `README.md` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* README.md

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (go)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=WJQSERVER-STUDIO/ghproxy&utm_content=108):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: WJQSERVER
created_at: 2025-05-21T04:13:38Z
body: /gemini review
author_association: MEMBER