package handler import ( "context" "time" "github.com/gin-gonic/gin" "nl-game-api-gin/internal/ai" "nl-game-api-gin/internal/config" "nl-game-api-gin/internal/model" "nl-game-api-gin/internal/service" "nl-game-api-gin/pkg/resp" ) // aiProviderConf 单个提供方的当前生效配置(后台展示用) type aiProviderConf struct { APIKey string `json:"api_key"` // 生效中的 API Key(仅超管可见) BaseURL string `json:"base_url"` // 生效中的接口地址 Model string `json:"model"` // 生效中的模型名 KeySource string `json:"key_source"` // Key 来源:后台配置/配置文件/未配置 } // effectiveAIConf 计算某提供方的生效配置:数据库非空项覆盖 config.yaml func effectiveAIConf(provider string) aiProviderConf { var base config.LLMConf var kKey, kBase, kModel string if provider == ai.ProviderSpark { base, kKey, kBase, kModel = config.C.AI.Spark, model.ConfKeyAISparkKey, model.ConfKeyAISparkBase, model.ConfKeyAISparkModel } else { base, kKey, kBase, kModel = config.C.AI.DeepSeek, model.ConfKeyAIDeepSeekKey, model.ConfKeyAIDeepSeekBase, model.ConfKeyAIDeepSeekModel } out := aiProviderConf{APIKey: base.APIKey, BaseURL: base.BaseURL, Model: base.Model, KeySource: "配置文件"} if v := service.GetConfig(kKey, ""); v != "" { out.APIKey, out.KeySource = v, "后台配置" } if v := service.GetConfig(kBase, ""); v != "" { out.BaseURL = v } if v := service.GetConfig(kModel, ""); v != "" { out.Model = v } if out.APIKey == "" { out.KeySource = "未配置" } return out } // AdminAIConfig 后台:查看当前全站AI模型选择、两家大模型的生效配置与获取指引 func AdminAIConfig(c *gin.Context) { resp.OK(c, gin.H{ "provider": ai.GlobalProvider(), // 全站统一的对战AI提供方(玩家前台只选难度) "spark": effectiveAIConf(ai.ProviderSpark), "deepseek": effectiveAIConf(ai.ProviderDeepSeek), // 申请指引(前端展示) "guide": gin.H{ "spark": "讯飞开放平台 console.xfyun.cn 创建应用后,在「Spark Lite → HTTP 服务接口认证信息」复制 APIPassword 填入即可,Lite 模型免费", "deepseek": "DeepSeek 开放平台 platform.deepseek.com 注册后创建 API Key(sk- 开头),按 token 计费", }, }) } // aiSaveReq 保存AI配置请求体(保存后立即生效,无需重启) type aiSaveReq struct { Provider *string `json:"provider"` // 全站AI提供方:rule/spark/deepseek(nil=不修改) Spark *struct { APIKey *string `json:"api_key"` // 留空=清除后台配置回退到配置文件 BaseURL *string `json:"base_url"` // 接口地址 Model *string `json:"model"` // 模型名 } `json:"spark"` DeepSeek *struct { APIKey *string `json:"api_key"` BaseURL *string `json:"base_url"` Model *string `json:"model"` } `json:"deepseek"` } // AdminSaveAIConfig 后台:保存大模型 API Key 等配置到数据库(覆盖 config.yaml) func AdminSaveAIConfig(c *gin.Context) { var req aiSaveReq if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, "参数有误") return } // 切换全站AI模型(新建房间立即生效,已开局房间不受影响) if req.Provider != nil { p := *req.Provider if p != ai.ProviderRule && p != ai.ProviderSpark && p != ai.ProviderDeepSeek { resp.Fail(c, "AI提供方只能是 rule / spark / deepseek") return } service.SetConfig(model.ConfKeyAIProvider, p) } if req.Spark != nil { saveIfSet(model.ConfKeyAISparkKey, req.Spark.APIKey) saveIfSet(model.ConfKeyAISparkBase, req.Spark.BaseURL) saveIfSet(model.ConfKeyAISparkModel, req.Spark.Model) } if req.DeepSeek != nil { saveIfSet(model.ConfKeyAIDeepSeekKey, req.DeepSeek.APIKey) saveIfSet(model.ConfKeyAIDeepSeekBase, req.DeepSeek.BaseURL) saveIfSet(model.ConfKeyAIDeepSeekModel, req.DeepSeek.Model) } resp.OK(c, gin.H{ "provider": ai.GlobalProvider(), "spark": effectiveAIConf(ai.ProviderSpark), "deepseek": effectiveAIConf(ai.ProviderDeepSeek), }) } // saveIfSet 请求里带了该字段才写库(nil=不动,空串=清除让其回退配置文件) func saveIfSet(key string, val *string) { if val != nil { service.SetConfig(key, *val) } } // aiTestReq 连通性测试请求体 type aiTestReq struct { Provider string `json:"provider" binding:"required,oneof=spark deepseek"` // 要测试的提供方 } // AdminTestAI 后台:向大模型发一条测试消息,验证 Key 是否可用并测量延迟 func AdminTestAI(c *gin.Context) { var req aiTestReq if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, "参数有误") return } client := ai.ClientForTest(req.Provider) if client == nil { resp.Fail(c, "该提供方还没有配置 API Key") return } ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) defer cancel() start := time.Now() reply, err := client.Chat(ctx, "你是连通性测试助手。", "请只回复两个字:在线", 0.1) if err != nil { resp.Fail(c, "连接失败:"+err.Error()) return } resp.OK(c, gin.H{ "reply": reply, // 模型回复内容 "latency_ms": time.Since(start).Milliseconds(), // 往返延迟(毫秒) }) }