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(), // 往返延迟(毫秒)
|
|||
|
|
})
|
|||
|
|
}
|