148 lines
5.1 KiB
Go
148 lines
5.1 KiB
Go
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(), // 往返延迟(毫秒)
|
||
})
|
||
}
|