diff --git a/backend/internal/api/handlers/llm.go b/backend/internal/api/handlers/llm.go index 6961635..8b3287a 100644 --- a/backend/internal/api/handlers/llm.go +++ b/backend/internal/api/handlers/llm.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "ezops/internal/api/middleware" "ezops/internal/models" "github.com/gin-gonic/gin" @@ -235,9 +236,23 @@ func (h *LLMHandler) Chat(c *gin.Context) { c.JSON(http.StatusOK, models.Response{Code: 500, Message: "解析 LLM 响应失败"}) return } + replyContent := chatResp.Choices[0].Message.Content + + // 持久化对话历史(按用户隔离,成对写入 user + assistant) // Logan.Li + // 仅在成功时入库;错误气泡为瞬时 UI 不持久化 + uid := middleware.GetUserID(c) + if uid != 0 && len(req.Messages) > 0 { + // 取最后一条用户消息(前端已附加当前提问) + lastUser := req.Messages[len(req.Messages)-1] + now := time.Now() + h.db.Create(&[]models.ChatHistory{ + {UserID: uid, Role: "user", Content: lastUser.Content, CreatedAt: now}, + {UserID: uid, Role: "assistant", Content: replyContent, Model: cfg.Model, CreatedAt: now.Add(time.Millisecond)}, + }) + } c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ChatResponse{ - Content: chatResp.Choices[0].Message.Content, + Content: replyContent, Model: cfg.Model, TokensUsed: chatResp.Usage.TotalTokens, }}) @@ -292,3 +307,34 @@ func (h *LLMHandler) TestLLMConnection(c *gin.Context) { } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "连接成功"}) } + +// GetChatHistory 获取当前用户的对话历史(按时间正序,仅返回最近 100 条避免过大) // Logan.Li +// GET /api/v1/aiops/chat-history +func (h *LLMHandler) GetChatHistory(c *gin.Context) { + uid := middleware.GetUserID(c) + var rows []models.ChatHistory + // 子查询取最近 100 条后再正序返回,保证前端渲染顺序正确 // Logan.Li + h.db.Where("user_id = ?", uid). + Order("id DESC"). + Limit(100). + Find(&rows) + // 反转为时间正序(最早在前) + for i, j := 0, len(rows)-1; i < j; i, j = i+1, j-1 { + rows[i], rows[j] = rows[j], rows[i] + } + c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{ + "list": rows, + "total": len(rows), + }}) +} + +// ClearChatHistory 清空当前用户的对话历史 // Logan.Li +// DELETE /api/v1/aiops/chat-history +func (h *LLMHandler) ClearChatHistory(c *gin.Context) { + uid := middleware.GetUserID(c) + if err := h.db.Where("user_id = ?", uid).Delete(&models.ChatHistory{}).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "清除失败: " + err.Error()}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "对话历史已清空"}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index b03c308..52ab525 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -421,6 +421,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { aiopsGroup.PUT("/llm-config", llmHandler.SaveLLMConfig) aiopsGroup.POST("/llm-config/test", llmHandler.TestLLMConnection) aiopsGroup.POST("/chat", llmHandler.Chat) + aiopsGroup.GET("/chat-history", llmHandler.GetChatHistory) + aiopsGroup.DELETE("/chat-history", llmHandler.ClearChatHistory) } // 启动告警引擎(每 30 秒检查一次) diff --git a/backend/internal/models/llm.go b/backend/internal/models/llm.go index 87d2931..ad9d6b6 100644 --- a/backend/internal/models/llm.go +++ b/backend/internal/models/llm.go @@ -50,3 +50,17 @@ type ChatResponse struct { Model string `json:"model"` TokensUsed int `json:"tokens_used"` } + +// ChatHistory 对话历史记录(按用户隔离) // Logan.Li +// 仅持久化成功完成的对话(用户消息 + 助手回复成对写入),错误气泡为瞬时 UI 不入库 +type ChatHistory struct { + ID uint `gorm:"primarykey" json:"id"` + UserID uint `gorm:"index;not null" json:"user_id"` // 所属用户(JWT 中间件注入) + Role string `gorm:"size:16;not null" json:"role"` // user / assistant + Content string `gorm:"type:text;not null" json:"content"` // 消息内容 + Model string `gorm:"size:100" json:"model"` // 生成回复所用模型(仅 assistant 行有意义) + CreatedAt time.Time `json:"created_at"` +} + +// TableName 表名 // Logan.Li +func (ChatHistory) TableName() string { return "chat_histories" } diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index b909561..02e25c4 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -103,5 +103,6 @@ func AutoMigrate(db *gorm.DB) error { &models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li &models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li &models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li + &models.ChatHistory{}, // 对话历史记录(按用户隔离) // Logan.Li ) } diff --git a/frontend/src/components/aiops/AIOpsAssistant.tsx b/frontend/src/components/aiops/AIOpsAssistant.tsx index 5dc740b..2bf6895 100644 --- a/frontend/src/components/aiops/AIOpsAssistant.tsx +++ b/frontend/src/components/aiops/AIOpsAssistant.tsx @@ -23,7 +23,7 @@ import { } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; -import { aiApi, type LLMConfigData } from '@/services/api'; +import { aiApi, type LLMConfigData, type ChatHistoryItem } from '@/services/api'; import { Bot, Send, @@ -110,7 +110,28 @@ export function AIOpsAssistant() { setConfigReady(false); } }; - useEffect(() => { loadConfig(); }, []); + + // 加载历史对话(按用户隔离,刷新后仍可见) // Logan.Li + const loadHistory = async () => { + try { + const res = await aiApi.getHistory(); + const items: ChatHistoryItem[] = (res as any)?.list || []; + setMessages( + items.map((m) => ({ + id: String(m.id), + role: m.role, + content: m.content, + timestamp: m.created_at, + })), + ); + } catch { + // 历史加载失败不阻塞主流程 + } + }; + useEffect(() => { + loadConfig(); + loadHistory(); + }, []); // 切换提供商时自动填充默认 endpoint + 模型 // Logan.Li const handleProviderChange = (provider: string) => { @@ -189,10 +210,16 @@ export function AIOpsAssistant() { } }; - // 新对话 // Logan.Li - const handleNewChat = () => { + // 新对话(清空本地 + 后端持久化历史) // Logan.Li + const handleNewChat = async () => { + try { + await aiApi.clearHistory(); + } catch { + // 清除失败不阻塞本地清空 + } setMessages([]); setInput(''); + toast.success('已开始新对话'); }; const currentProvider = LLM_PROVIDERS.find((p) => p.id === config.provider); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index b88033f..d442f2d 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -653,6 +653,16 @@ export interface ChatResult { tokens_used: number; } +// 对话历史条目(后端 ChatHistory 序列化) // Logan.Li +export interface ChatHistoryItem { + id: number; + user_id: number; + role: 'user' | 'assistant'; + content: string; + model?: string; + created_at: string; +} + export const aiApi = { // 获取 LLM 配置(API Key 已脱敏) getConfig: () => http.get('/aiops/llm-config'), @@ -663,4 +673,8 @@ export const aiApi = { // AI 对话(messages 含历史 + 当前用户消息) chat: (messages: { role: 'user' | 'assistant'; content: string }[]) => http.post('/aiops/chat', { messages }), + // 获取当前用户对话历史(按时间正序,最近 100 条) + getHistory: () => http.get<{ list: ChatHistoryItem[]; total: number }>('/aiops/chat-history'), + // 清空当前用户对话历史 + clearHistory: () => http.delete('/aiops/chat-history'), };