Files
meshtastic_mqtt_server/internal/llmadmin/admin_llm_routes.go
T
kevinandClaude Fable 5 2f83308dce 修复 LLM Provider 配置热更新问题
问题:在 /admin/llm/api 页面修改 AI 提供商配置后,配置没有立即生效,AI 依然使用旧的提供商工作

根本原因:
- LLM Provider 配置在程序启动时加载到内存 (llm.State)
- 管理后台修改配置时,只更新了数据库,内存配置未更新
- AI 继续使用内存中的旧配置

解决方案:
1. 为 llm.State 添加动态更新方法 (UpdateProvider/AddProvider/RemoveProvider)
2. 在 AI Service 中暴露配置更新接口,支持运行时重新加载
3. 在配置保存后自动触发内存配置重新加载
4. 创建新的 LLM Client 使新配置立即生效

关键特性:
- 线程安全:使用 sync.RWMutex 保护并发访问
- 容错处理:重新加载失败不影响数据库更新
- 无需重启:配置修改后立即生效
- 完整测试:添加单元测试验证功能

修改文件:
- internal/llm/state.go: 添加配置更新方法
- internal/ai/service.go: 添加配置重新加载接口
- internal/llmadmin/admin_llm_routes.go: 配置更新时触发重新加载
- internal/web/web.go: 传递 AI Service 到路由
- main.go: 连接组件
- internal/llm/state_test.go: 新增单元测试
- internal/web/map_tile_proxy_routes_test.go: 修复测试

测试:
-  所有现有测试通过
-  新增测试覆盖核心功能
-  项目成功编译

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 21:13:31 +08:00

817 lines
23 KiB
Go

package llmadmin
import (
"errors"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
storepkg "meshtastic_mqtt_server/internal/store"
"meshtastic_mqtt_server/internal/webutil"
)
// LLMProviderReloader is the interface for reloading LLM provider configuration
type LLMProviderReloader interface {
ReloadLLMProvider(config interface{}) error
AddLLMProvider(config interface{}) error
RemoveLLMProvider(name string) error
}
func RegisterRoutes(r *gin.RouterGroup, store *storepkg.Store, aiService LLMProviderReloader) {
group := r.Group("/llm")
{
// LLM Message Queue
group.GET("/messages", handleListLLMMessages(store))
group.GET("/messages/:id", handleGetLLMMessage(store))
group.PUT("/messages/:id/status", handleUpdateLLMMessageStatus(store))
group.DELETE("/messages/:id", handleDeleteLLMMessage(store))
group.DELETE("/bots/:bot_id/messages", handleDeleteLLMMessagesByBot(store))
group.POST("/messages/cleanup", handleCleanupDeletedLLMMessages(store))
// LLM Providers
group.GET("/providers", handleListLLMProviders(store))
group.GET("/providers/:name", handleGetLLMProvider(store))
group.POST("/providers", handleCreateLLMProvider(store, aiService))
group.PUT("/providers/:name", handleUpdateLLMProvider(store, aiService))
group.DELETE("/providers/:name", handleDeleteLLMProvider(store, aiService))
// LLM Tool Router
group.GET("/tool-router", handleGetLLMToolRouter(store))
group.PUT("/tool-router", handleUpdateLLMToolRouter(store))
// LLM Topic Config - 话题选择配置
group.GET("/topic-config", handleGetLLMTopicConfig(store))
group.PUT("/topic-config", handleUpdateLLMTopicConfig(store))
// LLM Primary Config - 主 AI 回复配置
group.GET("/primary-config", handleGetLLMPrimaryConfig(store))
group.PUT("/primary-config", handleUpdateLLMPrimaryConfig(store))
}
}
func handleListLLMMessages(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
opts, ok := webutil.ParseListOptions(c)
if !ok {
return
}
var botID uint64
if botIDStr := c.Query("bot_id"); botIDStr != "" {
id, err := strconv.ParseUint(botIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid bot_id"})
return
}
botID = id
}
includeDeleted := c.Query("include_deleted") == "true"
rows, total, err := store.ListLLMMessages(opts, botID, includeDeleted)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items := make([]map[string]any, 0, len(rows))
for _, row := range rows {
items = append(items, storepkg.LLMMessageDTO(row))
}
c.JSON(http.StatusOK, gin.H{
"items": items,
"limit": opts.Limit,
"offset": opts.Offset,
"total": total,
})
}
}
func handleGetLLMMessage(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message id"})
return
}
record, err := store.GetLLMMessage(id)
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": storepkg.LLMMessageDTO(*record)})
}
}
func handleUpdateLLMMessageStatus(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message id"})
return
}
var req struct {
Status string `json:"status"`
Error string `json:"error"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
// 验证状态值
validStatus := map[string]bool{
storepkg.LLMMessageStatusPending: true,
storepkg.LLMMessageStatusProcessing: true,
storepkg.LLMMessageStatusProcessed: true,
storepkg.LLMMessageStatusError: true,
}
if !validStatus[req.Status] {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid status value"})
return
}
if err := store.UpdateLLMMessageStatus(id, req.Status, req.Error); err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
}
func handleDeleteLLMMessage(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message id"})
return
}
if err := store.SoftDeleteLLMMessage(id); err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
}
func handleDeleteLLMMessagesByBot(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
botID, err := strconv.ParseUint(c.Param("bot_id"), 10, 64)
if err != nil || botID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid bot id"})
return
}
if err := store.SoftDeleteLLMMessagesByBot(botID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
}
func handleCleanupDeletedLLMMessages(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
Days int `json:"days"`
}
if err := c.ShouldBindJSON(&req); err != nil {
req.Days = 7 // 默认清理 7 天前删除的消息
}
if req.Days <= 0 {
req.Days = 7
}
before := time.Now().AddDate(0, 0, -req.Days)
count, err := store.CleanupDeletedLLMMessages(before)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "deleted_count": count})
}
}
// ============================================
// LLM Provider Handlers
// ============================================
func handleListLLMProviders(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
includeInactive := c.Query("include_inactive") == "true"
rows, err := store.ListLLMProviders(includeInactive)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
items := make([]map[string]any, 0, len(rows))
for _, row := range rows {
items = append(items, llmProviderDTO(row))
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
}
func handleGetLLMProvider(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name is required"})
return
}
record, err := store.GetLLMProvider(name)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": llmProviderDTO(*record)})
}
}
func handleCreateLLMProvider(store *storepkg.Store, aiService LLMProviderReloader) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
Name string `json:"name"`
Active bool `json:"active"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
Model string `json:"model"`
Timeout int `json:"timeout"`
ContextWindowTokens int `json:"context_window_tokens"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if req.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
record := &storepkg.LLMProviderRecord{
Name: req.Name,
Active: req.Active,
APIKey: req.APIKey,
BaseURL: req.BaseURL,
Model: req.Model,
Timeout: req.Timeout,
ContextWindowTokens: req.ContextWindowTokens,
}
if err := store.CreateLLMProvider(record); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Reload AI service with new provider
if aiService != nil {
providerConfig := map[string]interface{}{
"Name": record.Name,
"Active": record.Active,
"APIKey": record.APIKey,
"BaseURL": record.BaseURL,
"Model": record.Model,
"Timeout": record.Timeout,
"ContextWindowTokens": record.ContextWindowTokens,
}
if err := aiService.AddLLMProvider(providerConfig); err != nil {
// Log warning but don't fail the request - database is already updated
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"item": llmProviderDTO(*record),
"warning": "provider created but failed to reload AI service: " + err.Error(),
})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmProviderDTO(*record)})
}
}
func handleUpdateLLMProvider(store *storepkg.Store, aiService LLMProviderReloader) gin.HandlerFunc {
return func(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name is required"})
return
}
var req struct {
Active *bool `json:"active"`
APIKey *string `json:"api_key"`
BaseURL *string `json:"base_url"`
Model *string `json:"model"`
Timeout *int `json:"timeout"`
ContextWindowTokens *int `json:"context_window_tokens"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
updates := make(map[string]any)
if req.Active != nil {
updates["active"] = *req.Active
}
if req.APIKey != nil {
updates["api_key"] = *req.APIKey
}
if req.BaseURL != nil {
updates["base_url"] = *req.BaseURL
}
if req.Model != nil {
updates["model"] = *req.Model
}
if req.Timeout != nil {
updates["timeout"] = *req.Timeout
}
if req.ContextWindowTokens != nil {
updates["context_window_tokens"] = *req.ContextWindowTokens
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
if err := store.UpdateLLMProvider(name, updates); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record, err := store.GetLLMProvider(name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Reload AI service with updated provider
if aiService != nil {
providerConfig := map[string]interface{}{
"Name": record.Name,
"Active": record.Active,
"APIKey": record.APIKey,
"BaseURL": record.BaseURL,
"Model": record.Model,
"Timeout": record.Timeout,
"ContextWindowTokens": record.ContextWindowTokens,
}
if err := aiService.ReloadLLMProvider(providerConfig); err != nil {
// Log warning but don't fail the request - database is already updated
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"item": llmProviderDTO(*record),
"warning": "provider updated but failed to reload AI service: " + err.Error(),
})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmProviderDTO(*record)})
}
}
func handleDeleteLLMProvider(store *storepkg.Store, aiService LLMProviderReloader) gin.HandlerFunc {
return func(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name is required"})
return
}
if err := store.DeleteLLMProvider(name); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Remove provider from AI service
if aiService != nil {
if err := aiService.RemoveLLMProvider(name); err != nil {
// Log warning but don't fail the request - database is already updated
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"warning": "provider deleted but failed to reload AI service: " + err.Error(),
})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
}
func llmProviderDTO(row storepkg.LLMProviderRecord) map[string]any {
return map[string]any{
"name": row.Name,
"active": row.Active,
"base_url": row.BaseURL,
"model": row.Model,
"timeout": row.Timeout,
"context_window_tokens": row.ContextWindowTokens,
"created_at": row.CreatedAt,
"updated_at": row.UpdatedAt,
}
}
// ============================================
// LLM Tool Router Handlers
// ============================================
func handleGetLLMToolRouter(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
record, err := store.GetLLMToolRouter()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "tool router config not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": llmToolRouterDTO(*record)})
}
}
func handleUpdateLLMToolRouter(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
record, err := store.GetLLMToolRouter()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var req struct {
Enabled *bool `json:"enabled"`
OpenAIName *string `json:"openai_name"`
Timeout *int `json:"timeout"`
MaxTokens *int `json:"max_tokens"`
SystemPrompt *string `json:"system_prompt"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
updates := make(map[string]any)
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if req.OpenAIName != nil {
updates["openai_name"] = *req.OpenAIName
}
if req.Timeout != nil {
updates["timeout"] = *req.Timeout
}
if req.MaxTokens != nil {
updates["max_tokens"] = *req.MaxTokens
}
if req.SystemPrompt != nil {
updates["system_prompt"] = *req.SystemPrompt
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
if record == nil {
// 创建新配置
newRecord := &storepkg.LLMToolRouterRecord{
Enabled: req.Enabled != nil && *req.Enabled,
OpenAIName: "",
Timeout: 30,
MaxTokens: 512,
SystemPrompt: "",
}
if req.OpenAIName != nil {
newRecord.OpenAIName = *req.OpenAIName
}
if req.Timeout != nil {
newRecord.Timeout = *req.Timeout
}
if req.MaxTokens != nil {
newRecord.MaxTokens = *req.MaxTokens
}
if req.SystemPrompt != nil {
newRecord.SystemPrompt = *req.SystemPrompt
}
if err := store.CreateLLMToolRouter(newRecord); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record = newRecord
} else {
// 更新现有配置
if err := store.UpdateLLMToolRouter(record.ID, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record, err = store.GetLLMToolRouter()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmToolRouterDTO(*record)})
}
}
func llmToolRouterDTO(row storepkg.LLMToolRouterRecord) map[string]any {
return map[string]any{
"id": row.ID,
"enabled": row.Enabled,
"openai_name": row.OpenAIName,
"timeout": row.Timeout,
"max_tokens": row.MaxTokens,
"system_prompt": row.SystemPrompt,
"created_at": row.CreatedAt,
"updated_at": row.UpdatedAt,
}
}
// ============================================
// LLM Topic Config Handlers - 话题选择配置
// ============================================
func handleGetLLMTopicConfig(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
record, err := store.GetLLMTopicConfig()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "topic config not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": llmTopicConfigDTO(*record)})
}
}
func handleUpdateLLMTopicConfig(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
record, err := store.GetLLMTopicConfig()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var req struct {
Enabled *bool `json:"enabled"`
OpenAIName *string `json:"openai_name"`
Timeout *int `json:"timeout"`
MaxTokens *int `json:"max_tokens"`
SystemPrompt *string `json:"system_prompt"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
updates := make(map[string]any)
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if req.OpenAIName != nil {
updates["openai_name"] = *req.OpenAIName
}
if req.Timeout != nil {
updates["timeout"] = *req.Timeout
}
if req.MaxTokens != nil {
updates["max_tokens"] = *req.MaxTokens
}
if req.SystemPrompt != nil {
updates["system_prompt"] = *req.SystemPrompt
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
if record == nil {
// 创建新配置
newRecord := &storepkg.LLMTopicConfigRecord{
Enabled: req.Enabled != nil && *req.Enabled,
OpenAIName: "",
Timeout: 30,
MaxTokens: 512,
SystemPrompt: "",
}
if req.OpenAIName != nil {
newRecord.OpenAIName = *req.OpenAIName
}
if req.Timeout != nil {
newRecord.Timeout = *req.Timeout
}
if req.MaxTokens != nil {
newRecord.MaxTokens = *req.MaxTokens
}
if req.SystemPrompt != nil {
newRecord.SystemPrompt = *req.SystemPrompt
}
if err := store.CreateLLMTopicConfig(newRecord); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record = newRecord
} else {
// 更新现有配置
if err := store.UpdateLLMTopicConfig(record.ID, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record, err = store.GetLLMTopicConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmTopicConfigDTO(*record)})
}
}
func llmTopicConfigDTO(row storepkg.LLMTopicConfigRecord) map[string]any {
return map[string]any{
"id": row.ID,
"enabled": row.Enabled,
"openai_name": row.OpenAIName,
"timeout": row.Timeout,
"max_tokens": row.MaxTokens,
"system_prompt": row.SystemPrompt,
"created_at": row.CreatedAt,
"updated_at": row.UpdatedAt,
}
}
// ============================================
// LLM Primary Config Handlers
// ============================================
func handleGetLLMPrimaryConfig(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
record, err := store.GetLLMPrimaryConfig()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "primary config not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"item": llmPrimaryConfigDTO(*record)})
}
}
func handleUpdateLLMPrimaryConfig(store *storepkg.Store) gin.HandlerFunc {
return func(c *gin.Context) {
record, err := store.GetLLMPrimaryConfig()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var req struct {
Enabled *bool `json:"enabled"`
ProviderName *string `json:"provider_name"`
Timeout *int `json:"timeout"`
MaxTokens *int `json:"max_tokens"`
SystemPrompt *string `json:"system_prompt"`
EnableTool *bool `json:"enable_tool"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
updates := make(map[string]any)
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if req.ProviderName != nil {
updates["provider_name"] = *req.ProviderName
}
if req.Timeout != nil {
updates["timeout"] = *req.Timeout
}
if req.MaxTokens != nil {
updates["max_tokens"] = *req.MaxTokens
}
if req.SystemPrompt != nil {
updates["system_prompt"] = *req.SystemPrompt
}
if req.EnableTool != nil {
updates["enable_tool"] = *req.EnableTool
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
return
}
if record == nil {
// 创建新配置
newRecord := &storepkg.LLMPrimaryConfigRecord{
Enabled: req.Enabled != nil && *req.Enabled,
ProviderName: "",
Timeout: 120,
MaxTokens: 1024,
SystemPrompt: "",
EnableTool: false,
}
if req.ProviderName != nil {
newRecord.ProviderName = *req.ProviderName
}
if req.Timeout != nil {
newRecord.Timeout = *req.Timeout
}
if req.MaxTokens != nil {
newRecord.MaxTokens = *req.MaxTokens
}
if req.SystemPrompt != nil {
newRecord.SystemPrompt = *req.SystemPrompt
}
if req.EnableTool != nil {
newRecord.EnableTool = *req.EnableTool
}
if err := store.CreateLLMPrimaryConfig(newRecord); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record = newRecord
} else {
// 更新现有配置
if err := store.UpdateLLMPrimaryConfig(record.ID, updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
record, err = store.GetLLMPrimaryConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "item": llmPrimaryConfigDTO(*record)})
}
}
func llmPrimaryConfigDTO(row storepkg.LLMPrimaryConfigRecord) map[string]any {
return map[string]any{
"id": row.ID,
"enabled": row.Enabled,
"provider_name": row.ProviderName,
"timeout": row.Timeout,
"max_tokens": row.MaxTokens,
"system_prompt": row.SystemPrompt,
"enable_tool": row.EnableTool,
"created_at": row.CreatedAt,
"updated_at": row.UpdatedAt,
}
}