Files
nl-game-api/internal/handler/aiconf.go
2026-08-14 13:17:03 +08:00

148 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Keysk- 开头),按 token 计费",
},
})
}
// aiSaveReq 保存AI配置请求体保存后立即生效无需重启
type aiSaveReq struct {
Provider *string `json:"provider"` // 全站AI提供方rule/spark/deepseeknil=不修改)
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(), // 往返延迟(毫秒)
})
}