@@ -24,6 +24,7 @@ import (
2424 "github.com/codex2api/auth"
2525 "github.com/codex2api/cache"
2626 "github.com/codex2api/database"
27+ "github.com/codex2api/internal/imagestore"
2728 "github.com/codex2api/proxy"
2829 "github.com/codex2api/security"
2930 "github.com/codex2api/security/promptfilter"
@@ -140,6 +141,7 @@ func (h *Handler) RegisterRoutes(r *gin.Engine) {
140141 api .GET ("/ops/overview" , h .GetOpsOverview )
141142 api .GET ("/settings" , h .GetSettings )
142143 api .PUT ("/settings" , h .UpdateSettings )
144+ api .POST ("/settings/image-storage/test" , h .TestImageStorageConnection )
143145 api .GET ("/prompt-filter/logs" , h .ListPromptFilterLogs )
144146 api .DELETE ("/prompt-filter/logs" , h .ClearPromptFilterLogs )
145147 api .POST ("/prompt-filter/test" , h .TestPromptFilter )
@@ -2202,6 +2204,14 @@ type settingsResponse struct {
22022204 UsageLogFlushIntervalSeconds int `json:"usage_log_flush_interval_seconds"`
22032205 StreamFlushPolicy string `json:"stream_flush_policy"`
22042206 StreamFlushIntervalMS int `json:"stream_flush_interval_ms"`
2207+ ImageStorageBackend string `json:"image_storage_backend"`
2208+ ImageS3Endpoint string `json:"image_s3_endpoint"`
2209+ ImageS3Region string `json:"image_s3_region"`
2210+ ImageS3Bucket string `json:"image_s3_bucket"`
2211+ ImageS3AccessKey string `json:"image_s3_access_key"`
2212+ ImageS3SecretKey string `json:"image_s3_secret_key"`
2213+ ImageS3Prefix string `json:"image_s3_prefix"`
2214+ ImageS3ForcePathStyle bool `json:"image_s3_force_path_style"`
22052215}
22062216
22072217type updateSettingsReq struct {
@@ -2245,6 +2255,14 @@ type updateSettingsReq struct {
22452255 UsageLogFlushIntervalSeconds * int `json:"usage_log_flush_interval_seconds"`
22462256 StreamFlushPolicy * string `json:"stream_flush_policy"`
22472257 StreamFlushIntervalMS * int `json:"stream_flush_interval_ms"`
2258+ ImageStorageBackend * string `json:"image_storage_backend"`
2259+ ImageS3Endpoint * string `json:"image_s3_endpoint"`
2260+ ImageS3Region * string `json:"image_s3_region"`
2261+ ImageS3Bucket * string `json:"image_s3_bucket"`
2262+ ImageS3AccessKey * string `json:"image_s3_access_key"`
2263+ ImageS3SecretKey * string `json:"image_s3_secret_key"`
2264+ ImageS3Prefix * string `json:"image_s3_prefix"`
2265+ ImageS3ForcePathStyle * bool `json:"image_s3_force_path_style"`
22482266}
22492267
22502268// GetSettings 获取当前系统设置
@@ -2264,6 +2282,8 @@ func (h *Handler) GetSettings(c *gin.Context) {
22642282 }
22652283 promptFilterCfg := h .store .GetPromptFilterConfig ()
22662284 runtimeCfg := proxy .CurrentRuntimeSettings ()
2285+ imgCfg := imagestore .CurrentConfig ()
2286+ imgPrefix := strings .TrimSuffix (imgCfg .Prefix , "/" )
22672287 c .JSON (http .StatusOK , settingsResponse {
22682288 MaxConcurrency : h .store .GetMaxConcurrency (),
22692289 GlobalRPM : h .rateLimiter .GetRPM (),
@@ -2310,6 +2330,14 @@ func (h *Handler) GetSettings(c *gin.Context) {
23102330 UsageLogFlushIntervalSeconds : h .db .GetUsageLogFlushIntervalSeconds (),
23112331 StreamFlushPolicy : runtimeCfg .StreamFlushPolicy ,
23122332 StreamFlushIntervalMS : runtimeCfg .StreamFlushIntervalMS ,
2333+ ImageStorageBackend : imgCfg .Backend ,
2334+ ImageS3Endpoint : imgCfg .Endpoint ,
2335+ ImageS3Region : imgCfg .Region ,
2336+ ImageS3Bucket : imgCfg .Bucket ,
2337+ ImageS3AccessKey : imgCfg .AccessKey ,
2338+ ImageS3SecretKey : imgCfg .SecretKey ,
2339+ ImageS3Prefix : imgPrefix ,
2340+ ImageS3ForcePathStyle : imgCfg .ForcePathStyle ,
23132341 })
23142342}
23152343
@@ -2655,6 +2683,57 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
26552683 }
26562684 }
26572685
2686+ // 图片存储后端配置
2687+ imgCfg := imagestore .CurrentConfig ()
2688+ imgChanged := false
2689+ if req .ImageStorageBackend != nil {
2690+ imgCfg .Backend = * req .ImageStorageBackend
2691+ imgChanged = true
2692+ }
2693+ if req .ImageS3Endpoint != nil {
2694+ imgCfg .Endpoint = * req .ImageS3Endpoint
2695+ imgChanged = true
2696+ }
2697+ if req .ImageS3Region != nil {
2698+ imgCfg .Region = * req .ImageS3Region
2699+ imgChanged = true
2700+ }
2701+ if req .ImageS3Bucket != nil {
2702+ imgCfg .Bucket = * req .ImageS3Bucket
2703+ imgChanged = true
2704+ }
2705+ if req .ImageS3AccessKey != nil {
2706+ imgCfg .AccessKey = * req .ImageS3AccessKey
2707+ imgChanged = true
2708+ }
2709+ if req .ImageS3SecretKey != nil {
2710+ imgCfg .SecretKey = * req .ImageS3SecretKey
2711+ imgChanged = true
2712+ }
2713+ if req .ImageS3Prefix != nil {
2714+ imgCfg .Prefix = * req .ImageS3Prefix
2715+ imgChanged = true
2716+ }
2717+ if req .ImageS3ForcePathStyle != nil {
2718+ imgCfg .ForcePathStyle = * req .ImageS3ForcePathStyle
2719+ imgChanged = true
2720+ }
2721+ imgCfg .LocalDir = imageAssetDir ()
2722+ if imgChanged {
2723+ if err := imagestore .Configure (imgCfg ); err != nil {
2724+ writeError (c , http .StatusBadRequest , "图片存储配置无效: " + err .Error ())
2725+ return
2726+ }
2727+ // Configure 内部 Normalize 过,重新读出来用于持久化
2728+ imgCfg = imagestore .CurrentConfig ()
2729+ log .Printf ("设置已更新: image_storage_backend = %s" , imgCfg .Backend )
2730+ }
2731+ imgConfigJSON , encodeErr := imagestore .EncodeConfigJSON (imgCfg )
2732+ if encodeErr != nil {
2733+ log .Printf ("图片存储配置序列化失败: %v" , encodeErr )
2734+ imgConfigJSON = "{}"
2735+ }
2736+
26582737 // 持久化保存到数据库
26592738 err := h .db .UpdateSystemSettings (c .Request .Context (), & database.SystemSettings {
26602739 MaxConcurrency : h .store .GetMaxConcurrency (),
@@ -2697,6 +2776,7 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
26972776 UsageLogFlushIntervalSeconds : usageLogFlushIntervalSeconds ,
26982777 StreamFlushPolicy : runtimeCfg .StreamFlushPolicy ,
26992778 StreamFlushIntervalMS : runtimeCfg .StreamFlushIntervalMS ,
2779+ ImageStorageConfig : imgConfigJSON ,
27002780 })
27012781 if err != nil {
27022782 log .Printf ("无法持久化保存设置: %v" , err )
@@ -2762,9 +2842,63 @@ func (h *Handler) UpdateSettings(c *gin.Context) {
27622842 UsageLogFlushIntervalSeconds : usageLogFlushIntervalSeconds ,
27632843 StreamFlushPolicy : runtimeCfg .StreamFlushPolicy ,
27642844 StreamFlushIntervalMS : runtimeCfg .StreamFlushIntervalMS ,
2845+ ImageStorageBackend : imgCfg .Backend ,
2846+ ImageS3Endpoint : imgCfg .Endpoint ,
2847+ ImageS3Region : imgCfg .Region ,
2848+ ImageS3Bucket : imgCfg .Bucket ,
2849+ ImageS3AccessKey : imgCfg .AccessKey ,
2850+ ImageS3SecretKey : imgCfg .SecretKey ,
2851+ ImageS3Prefix : strings .TrimSuffix (imgCfg .Prefix , "/" ),
2852+ ImageS3ForcePathStyle : imgCfg .ForcePathStyle ,
27652853 })
27662854}
27672855
2856+ type testImageStorageReq struct {
2857+ Endpoint string `json:"endpoint"`
2858+ Region string `json:"region"`
2859+ Bucket string `json:"bucket"`
2860+ AccessKey string `json:"access_key"`
2861+ SecretKey string `json:"secret_key"`
2862+ Prefix string `json:"prefix"`
2863+ ForcePathStyle bool `json:"force_path_style"`
2864+ }
2865+
2866+ // TestImageStorageConnection 用提交的字段临时构造一次 S3Backend,调用 HeadBucket 验证可达性。
2867+ // 不修改任何持久化状态,便于"保存前先点测试连接"。
2868+ func (h * Handler ) TestImageStorageConnection (c * gin.Context ) {
2869+ var req testImageStorageReq
2870+ if err := c .ShouldBindJSON (& req ); err != nil {
2871+ writeError (c , http .StatusBadRequest , "请求格式错误" )
2872+ return
2873+ }
2874+ cfg := imagestore.Config {
2875+ Backend : imagestore .BackendS3 ,
2876+ Endpoint : req .Endpoint ,
2877+ Region : req .Region ,
2878+ Bucket : req .Bucket ,
2879+ AccessKey : req .AccessKey ,
2880+ SecretKey : req .SecretKey ,
2881+ Prefix : req .Prefix ,
2882+ ForcePathStyle : req .ForcePathStyle ,
2883+ }.Normalize ()
2884+ if err := cfg .Validate (); err != nil {
2885+ writeError (c , http .StatusBadRequest , err .Error ())
2886+ return
2887+ }
2888+ backend , err := imagestore .NewS3Backend (cfg )
2889+ if err != nil {
2890+ writeError (c , http .StatusBadRequest , err .Error ())
2891+ return
2892+ }
2893+ ctx , cancel := context .WithTimeout (c .Request .Context (), 10 * time .Second )
2894+ defer cancel ()
2895+ if err := backend .HeadBucket (ctx ); err != nil {
2896+ writeError (c , http .StatusBadRequest , err .Error ())
2897+ return
2898+ }
2899+ c .JSON (http .StatusOK , gin.H {"ok" : true , "bucket" : cfg .Bucket })
2900+ }
2901+
27682902// ==================== 导出 & 迁移 ====================
27692903
27702904type cpaExportEntry struct {
0 commit comments