feat: AIOps智能助手对话历史持久化(按用户隔离,刷新后仍可见)
This commit is contained in:
1 parent
6321886b69
commit
32de3709ed
6 files changed
+109
-5
No files matched your search
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/api/middleware"
|
||||||
"ezops/internal/models"
|
"ezops/internal/models"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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 响应失败"})
|
c.JSON(http.StatusOK, models.Response{Code: 500, Message: "解析 LLM 响应失败"})
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ChatResponse{
|
||||||
Content: chatResp.Choices[0].Message.Content,
|
Content: replyContent,
|
||||||
Model: cfg.Model,
|
Model: cfg.Model,
|
||||||
TokensUsed: chatResp.Usage.TotalTokens,
|
TokensUsed: chatResp.Usage.TotalTokens,
|
||||||
}})
|
}})
|
||||||
@@ -292,3 +307,34 @@ func (h *LLMHandler) TestLLMConnection(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "连接成功"})
|
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: "对话历史已清空"})
|
||||||
|
}
|
||||||
@@ -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.PUT("/llm-config", llmHandler.SaveLLMConfig)
|
||||||
aiopsGroup.POST("/llm-config/test", llmHandler.TestLLMConnection)
|
aiopsGroup.POST("/llm-config/test", llmHandler.TestLLMConnection)
|
||||||
aiopsGroup.POST("/chat", llmHandler.Chat)
|
aiopsGroup.POST("/chat", llmHandler.Chat)
|
||||||
|
aiopsGroup.GET("/chat-history", llmHandler.GetChatHistory)
|
||||||
|
aiopsGroup.DELETE("/chat-history", llmHandler.ClearChatHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动告警引擎(每 30 秒检查一次)
|
// 启动告警引擎(每 30 秒检查一次)
|
||||||
|
|||||||
@@ -50,3 +50,17 @@ type ChatResponse struct {
|
|||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
TokensUsed int `json:"tokens_used"`
|
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" }
|
||||||
@@ -103,5 +103,6 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
&models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
|
&models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li
|
||||||
&models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
|
&models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li
|
||||||
&models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li
|
&models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li
|
||||||
|
&models.ChatHistory{}, // 对话历史记录(按用户隔离) // Logan.Li
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { aiApi, type LLMConfigData } from '@/services/api';
|
import { aiApi, type LLMConfigData, type ChatHistoryItem } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
Send,
|
Send,
|
||||||
@@ -110,7 +110,28 @@ export function AIOpsAssistant() {
|
|||||||
setConfigReady(false);
|
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
|
// 切换提供商时自动填充默认 endpoint + 模型 // Logan.Li
|
||||||
const handleProviderChange = (provider: string) => {
|
const handleProviderChange = (provider: string) => {
|
||||||
@@ -189,10 +210,16 @@ export function AIOpsAssistant() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新对话 // Logan.Li
|
// 新对话(清空本地 + 后端持久化历史) // Logan.Li
|
||||||
const handleNewChat = () => {
|
const handleNewChat = async () => {
|
||||||
|
try {
|
||||||
|
await aiApi.clearHistory();
|
||||||
|
} catch {
|
||||||
|
// 清除失败不阻塞本地清空
|
||||||
|
}
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setInput('');
|
setInput('');
|
||||||
|
toast.success('已开始新对话');
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentProvider = LLM_PROVIDERS.find((p) => p.id === config.provider);
|
const currentProvider = LLM_PROVIDERS.find((p) => p.id === config.provider);
|
||||||
|
|||||||
@@ -653,6 +653,16 @@ export interface ChatResult {
|
|||||||
tokens_used: number;
|
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 = {
|
export const aiApi = {
|
||||||
// 获取 LLM 配置(API Key 已脱敏)
|
// 获取 LLM 配置(API Key 已脱敏)
|
||||||
getConfig: () => http.get<LLMConfigData>('/aiops/llm-config'),
|
getConfig: () => http.get<LLMConfigData>('/aiops/llm-config'),
|
||||||
@@ -663,4 +673,8 @@ export const aiApi = {
|
|||||||
// AI 对话(messages 含历史 + 当前用户消息)
|
// AI 对话(messages 含历史 + 当前用户消息)
|
||||||
chat: (messages: { role: 'user' | 'assistant'; content: string }[]) =>
|
chat: (messages: { role: 'user' | 'assistant'; content: string }[]) =>
|
||||||
http.post<ChatResult>('/aiops/chat', { messages }),
|
http.post<ChatResult>('/aiops/chat', { messages }),
|
||||||
|
// 获取当前用户对话历史(按时间正序,最近 100 条)
|
||||||
|
getHistory: () => http.get<{ list: ChatHistoryItem[]; total: number }>('/aiops/chat-history'),
|
||||||
|
// 清空当前用户对话历史
|
||||||
|
clearHistory: () => http.delete('/aiops/chat-history'),
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user