Files
nl-im-service/internal/api/ai_handler.go
2026-08-24 15:29:53 +08:00

250 lines
6.4 KiB
Go
Raw 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 api
* 作用AI 机器人相关 HTTP 接口。
*
* 权限模型:
* - AI 配置读写、机器人增删改:仅管理员(用户 id=1
* - 机器人列表查询:所有登录用户(好友列表要展示机器人)。
*/
package api
import (
"context"
"strconv"
"strings"
"time"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
)
/**
* requireAdmin
* 作用校验当前登录用户是否为管理员id=1
* 返回 false 时已写入 403 响应,调用方直接 return 即可
*/
func requireAdmin(c *gin.Context) bool {
userID, exists := c.Get("user_id")
if !exists || userID.(string) != service.AdminUserID {
utils.Error(c, utils.CodeForbidden, "仅管理员可操作")
return false
}
return true
}
/**
* maskAPIKey
* 作用API 密钥脱敏保留前4后4位配置回显时避免明文泄露
*/
func maskAPIKey(key string) string {
if key == "" {
return ""
}
if len(key) <= 8 {
return "********"
}
return key[:4] + strings.Repeat("*", 8) + key[len(key)-4:]
}
/**
* GetAIConfigHandler
* 功能:获取全局 AI 配置(密钥脱敏返回)
* 路径GET /api/ai/config (管理员)
*/
func GetAIConfigHandler(c *gin.Context) {
if !requireAdmin(c) {
return
}
cfg, err := service.AIBotSvc.GetAIConfig()
if err != nil {
utils.InternalError(c, "配置读取失败")
return
}
utils.SuccessWithData(c, gin.H{
"provider": cfg.Provider,
"base_url": cfg.BaseURL,
"api_key": maskAPIKey(cfg.APIKey),
"model": cfg.Model,
"enabled": cfg.Enabled,
// 前端据此判断"密钥是否已配置过"(脱敏值不能用于判断)
"has_key": cfg.APIKey != "",
}, "获取成功")
}
// saveAIConfigReq 保存 AI 配置请求体
type saveAIConfigReq struct {
Provider string `json:"provider" binding:"required"`
BaseURL string `json:"base_url"`
// 为空表示不修改已保存的密钥(前端回显的是脱敏值)
APIKey string `json:"api_key"`
Model string `json:"model" binding:"required"`
Enabled bool `json:"enabled"`
}
/**
* SaveAIConfigHandler
* 功能:保存全局 AI 配置
* 路径POST /api/ai/config (管理员)
*/
func SaveAIConfigHandler(c *gin.Context) {
if !requireAdmin(c) {
return
}
var req saveAIConfigReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
// 前端可能把脱敏值原样提交回来,含 **** 的一律视为"未修改"
if strings.Contains(req.APIKey, "****") {
req.APIKey = ""
}
cfg, err := service.AIBotSvc.SaveAIConfig(req.Provider, req.BaseURL, req.APIKey, req.Model, req.Enabled)
if err != nil {
utils.InternalError(c, "保存失败: "+err.Error())
return
}
utils.SuccessWithData(c, gin.H{
"provider": cfg.Provider,
"base_url": cfg.BaseURL,
"api_key": maskAPIKey(cfg.APIKey),
"model": cfg.Model,
"enabled": cfg.Enabled,
"has_key": cfg.APIKey != "",
}, "保存成功")
}
/**
* TestAIConfigHandler
* 功能:测试当前 AI 配置连通性(发一条固定问候语,成功返回模型回复)
* 路径POST /api/ai/config/test (管理员)
*/
func TestAIConfigHandler(c *gin.Context) {
if !requireAdmin(c) {
return
}
cfg, err := service.AIBotSvc.GetAIConfig()
if err != nil {
utils.InternalError(c, "配置读取失败")
return
}
provider, err := service.NewAIProvider(cfg)
if err != nil {
utils.BadRequest(c, err.Error())
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
reply, err := provider.Chat(ctx, []service.AIMessage{
{Role: "user", Content: "你好,请用一句话介绍你自己"},
})
if err != nil {
utils.BadRequest(c, "测试失败: "+err.Error())
return
}
utils.SuccessWithData(c, gin.H{"reply": reply}, "测试成功")
}
/**
* ListAIBotsHandler
* 功能:机器人列表。管理员传 all=1 返回全部(含停用),普通用户只见启用中的
* 路径GET /api/ai/bots (登录用户)
*/
func ListAIBotsHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
onlyEnabled := true
if c.Query("all") == "1" && userID != nil && userID.(string) == service.AdminUserID {
onlyEnabled = false
}
bots, err := service.AIBotSvc.ListBots(onlyEnabled)
if err != nil {
utils.InternalError(c, "查询失败")
return
}
utils.SuccessWithData(c, bots, "获取成功")
}
// botReq 创建/更新机器人请求体
type botReq struct {
Name string `json:"name" binding:"required"`
Avatar string `json:"avatar"`
RolePrompt string `json:"role_prompt"`
Enabled *bool `json:"enabled"`
}
/**
* CreateAIBotHandler
* 功能:创建机器人(同时生成 users 虚拟用户)
* 路径POST /api/ai/bots (管理员)
*/
func CreateAIBotHandler(c *gin.Context) {
if !requireAdmin(c) {
return
}
var req botReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
bot, err := service.AIBotSvc.CreateBot(req.Name, req.Avatar, req.RolePrompt)
if err != nil {
utils.InternalError(c, "创建失败: "+err.Error())
return
}
utils.SuccessWithData(c, bot, "创建成功")
}
/**
* UpdateAIBotHandler
* 功能:更新机器人(名称/头像/角色设定/启停)
* 路径POST /api/ai/bots/update/:id (管理员)
*/
func UpdateAIBotHandler(c *gin.Context) {
if !requireAdmin(c) {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
utils.BadRequest(c, "机器人ID不合法")
return
}
var req botReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
enabled := true
if req.Enabled != nil {
enabled = *req.Enabled
}
bot, err := service.AIBotSvc.UpdateBot(uint(id), req.Name, req.Avatar, req.RolePrompt, enabled)
if err != nil {
utils.InternalError(c, "更新失败: "+err.Error())
return
}
utils.SuccessWithData(c, bot, "更新成功")
}
/**
* DeleteAIBotHandler
* 功能:删除机器人(连带删除虚拟用户与群成员关系)
* 路径POST /api/ai/bots/delete/:id (管理员)
*/
func DeleteAIBotHandler(c *gin.Context) {
if !requireAdmin(c) {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
utils.BadRequest(c, "机器人ID不合法")
return
}
if err := service.AIBotSvc.DeleteBot(uint(id)); err != nil {
utils.InternalError(c, "删除失败: "+err.Error())
return
}
utils.Success(c, "删除成功")
}