修复 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>
This commit is contained in:
2026-06-23 21:13:31 +08:00
co-authored by Claude Fable 5
parent c50c7a57d7
commit 2f83308dce
8 changed files with 561 additions and 18 deletions
+70 -7
View File
@@ -13,7 +13,14 @@ import (
"meshtastic_mqtt_server/internal/webutil"
)
func RegisterRoutes(r *gin.RouterGroup, store *storepkg.Store) {
// 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
@@ -27,9 +34,9 @@ func RegisterRoutes(r *gin.RouterGroup, store *storepkg.Store) {
// LLM Providers
group.GET("/providers", handleListLLMProviders(store))
group.GET("/providers/:name", handleGetLLMProvider(store))
group.POST("/providers", handleCreateLLMProvider(store))
group.PUT("/providers/:name", handleUpdateLLMProvider(store))
group.DELETE("/providers/:name", handleDeleteLLMProvider(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))
@@ -254,7 +261,7 @@ func handleGetLLMProvider(store *storepkg.Store) gin.HandlerFunc {
}
}
func handleCreateLLMProvider(store *storepkg.Store) gin.HandlerFunc {
func handleCreateLLMProvider(store *storepkg.Store, aiService LLMProviderReloader) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
Name string `json:"name"`
@@ -290,11 +297,33 @@ func handleCreateLLMProvider(store *storepkg.Store) gin.HandlerFunc {
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) gin.HandlerFunc {
func handleUpdateLLMProvider(store *storepkg.Store, aiService LLMProviderReloader) gin.HandlerFunc {
return func(c *gin.Context) {
name := c.Param("name")
if name == "" {
@@ -355,11 +384,33 @@ func handleUpdateLLMProvider(store *storepkg.Store) gin.HandlerFunc {
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) gin.HandlerFunc {
func handleDeleteLLMProvider(store *storepkg.Store, aiService LLMProviderReloader) gin.HandlerFunc {
return func(c *gin.Context) {
name := c.Param("name")
if name == "" {
@@ -372,6 +423,18 @@ func handleDeleteLLMProvider(store *storepkg.Store) gin.HandlerFunc {
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"})
}
}