diff --git a/backend/internal/api/handlers/llm.go b/backend/internal/api/handlers/llm.go new file mode 100644 index 0000000..6961635 --- /dev/null +++ b/backend/internal/api/handlers/llm.go @@ -0,0 +1,294 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "ezops/internal/models" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// LLMHandler 大语言模型配置 + 对话代理 // Logan.Li +type LLMHandler struct { + db *gorm.DB + client *http.Client +} + +// NewLLMHandler 创建 LLM 处理器 // Logan.Li +func NewLLMHandler(db *gorm.DB) *LLMHandler { + return &LLMHandler{ + db: db, + // 超时控制:LLM 推理可能较慢,给 120s + client: &http.Client{Timeout: 120 * time.Second}, + } +} + +// maskAPIKey 脱敏 API Key(仅保留后 4 位) // Logan.Li +func maskAPIKey(key string) string { + if len(key) <= 4 { + return strings.Repeat("*", len(key)) + } + return strings.Repeat("*", len(key)-4) + key[len(key)-4:] +} + +// getActiveConfig 读取启用的配置(脱敏后) // Logan.Li +func (h *LLMHandler) getActiveConfig(maskKey bool) (models.LLMConfig, error) { + var cfg models.LLMConfig + if err := h.db.First(&cfg).Error; err != nil { + return cfg, err + } + if maskKey { + cfg.APIKey = maskAPIKey(cfg.APIKey) + } + return cfg, nil +} + +// GetLLMConfig 获取 LLM 配置(API Key 脱敏) // Logan.Li +// GET /api/v1/aiops/llm-config +func (h *LLMHandler) GetLLMConfig(c *gin.Context) { + cfg, err := h.getActiveConfig(true) + if err != nil { + // 未配置返回空对象(前端渲染空表单) + c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{ + "provider": "", "model": "", "api_key": "", + "endpoint": "", "temperature": 0.7, "max_tokens": 2000, + "system_prompt": "", "enabled": false, "configured": false, + }}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{ + "id": cfg.ID, + "provider": cfg.Provider, + "model": cfg.Model, + "api_key": cfg.APIKey, + "endpoint": cfg.Endpoint, + "temperature": cfg.Temperature, + "max_tokens": cfg.MaxTokens, + "system_prompt": cfg.SystemPrompt, + "enabled": cfg.Enabled, + "configured": true, + }}) +} + +// SaveLLMConfig 保存 LLM 配置(upsert 单行) // Logan.Li +// PUT /api/v1/aiops/llm-config +func (h *LLMHandler) SaveLLMConfig(c *gin.Context) { + var req models.SaveLLMConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()}) + return + } + var cfg models.LLMConfig + h.db.First(&cfg) + // 若前端回传脱敏值(含 ****),保留原 Key + if strings.Contains(req.APIKey, "****") && cfg.ID != 0 { + req.APIKey = cfg.APIKey + } + cfg.Provider = req.Provider + cfg.Model = req.Model + cfg.APIKey = req.APIKey + cfg.Endpoint = req.Endpoint + cfg.Temperature = req.Temperature + cfg.MaxTokens = req.MaxTokens + cfg.SystemPrompt = req.SystemPrompt + cfg.Enabled = req.Enabled + if cfg.ID == 0 { + if err := h.db.Create(&cfg).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()}) + return + } + } else { + if err := h.db.Save(&cfg).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()}) + return + } + } + // 返回脱敏后的配置 + cfg.APIKey = maskAPIKey(cfg.APIKey) + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "保存成功", Data: gin.H{ + "id": cfg.ID, + "provider": cfg.Provider, + "model": cfg.Model, + "api_key": cfg.APIKey, + "endpoint": cfg.Endpoint, + "temperature": cfg.Temperature, + "max_tokens": cfg.MaxTokens, + "system_prompt": cfg.SystemPrompt, + "enabled": cfg.Enabled, + "configured": true, + }}) +} + +// OpenAI 兼容请求/响应结构(仅取必要字段) // Logan.Li +type openaiChatRequest struct { + Model string `json:"model"` + Messages []models.ChatMessage `json:"messages"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"max_tokens,omitempty"` +} +type openaiChoice struct { + Message models.ChatMessage `json:"message"` +} +type openaiChatResponse struct { + Choices []openaiChoice `json:"choices"` + Usage struct { + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +// resolveEndpoint 根据提供商补全默认 endpoint // Logan.Li +func resolveEndpoint(provider, endpoint string) string { + if endpoint != "" { + return strings.TrimRight(endpoint, "/") + } + switch provider { + case "deepseek": + return "https://api.deepseek.com/v1" + case "qwen": + return "https://dashscope.aliyuncs.com/compatible-mode/v1" + case "moonshot": + return "https://api.moonshot.cn/v1" + case "ollama": + return "http://localhost:11434/v1" + default: // openai / custom + return "https://api.openai.com/v1" + } +} + +// defaultSystemPrompt AIOps 运维助手默认系统提示词 // Logan.Li +const defaultSystemPrompt = `你是 EZOps AIOps 智能运维助手,专注于企业级运维场景。你可以: +1. 分析服务器、容器、K8s 集群的性能瓶颈与异常 +2. 解读告警事件并给出根因分析与处置建议 +3. 生成 Shell / Ansible / Python 运维脚本 +4. 提供容量规划、成本优化、高可用架构建议 +回答请简洁、专业、可执行,优先给出具体命令或步骤。使用中文回答。` + +// Chat AI 对话代理(OpenAI 兼容接口) // Logan.Li +// POST /api/v1/aiops/chat +func (h *LLMHandler) Chat(c *gin.Context) { + var cfg models.LLMConfig + if err := h.db.First(&cfg).Error; err != nil || !cfg.Enabled { + c.JSON(http.StatusOK, models.Response{Code: 400, Message: "LLM 服务未配置或未启用,请在「模型配置」中完成设置"}) + return + } + + var req models.ChatRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误: " + err.Error()}) + return + } + + // 组装系统提示词 + systemPrompt := cfg.SystemPrompt + if strings.TrimSpace(systemPrompt) == "" { + systemPrompt = defaultSystemPrompt + } + messages := append([]models.ChatMessage{{Role: "system", Content: systemPrompt}}, req.Messages...) + + // 构造 OpenAI 兼容请求 + payload := openaiChatRequest{ + Model: cfg.Model, + Messages: messages, + Temperature: cfg.Temperature, + MaxTokens: cfg.MaxTokens, + } + body, _ := json.Marshal(payload) + + endpoint := resolveEndpoint(cfg.Provider, cfg.Endpoint) + url := endpoint + "/chat/completions" + + httpReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + c.JSON(http.StatusOK, models.Response{Code: 500, Message: "构造请求失败: " + err.Error()}) + return + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey) + + resp, err := h.client.Do(httpReq) + if err != nil { + c.JSON(http.StatusOK, models.Response{Code: 502, Message: "LLM 服务连接失败,请检查网络或 Endpoint 配置: " + err.Error()}) + return + } + defer resp.Body.Close() + + raw, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + // 提取上游错误信息 + msg := fmt.Sprintf("LLM 服务返回错误 (HTTP %d)", resp.StatusCode) + if upErr := parseOpenAIError(raw); upErr != "" { + msg += ": " + upErr + } + c.JSON(http.StatusOK, models.Response{Code: resp.StatusCode, Message: msg}) + return + } + + var chatResp openaiChatResponse + if err := json.Unmarshal(raw, &chatResp); err != nil || len(chatResp.Choices) == 0 { + c.JSON(http.StatusOK, models.Response{Code: 500, Message: "解析 LLM 响应失败"}) + return + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ChatResponse{ + Content: chatResp.Choices[0].Message.Content, + Model: cfg.Model, + TokensUsed: chatResp.Usage.TotalTokens, + }}) +} + +// parseOpenAIError 从上游错误响应中提取 message 字段 // Logan.Li +func parseOpenAIError(raw []byte) string { + var m struct { + Error struct { + Message string `json:"message"` + } `json:"error"` + } + if json.Unmarshal(raw, &m) == nil && m.Error.Message != "" { + return m.Error.Message + } + return "" +} + +// TestLLMConnection 测试 LLM 连接(发送一条 ping 消息) // Logan.Li +// POST /api/v1/aiops/llm-config/test +func (h *LLMHandler) TestLLMConnection(c *gin.Context) { + var cfg models.LLMConfig + if err := h.db.First(&cfg).Error; err != nil { + c.JSON(http.StatusOK, models.Response{Code: 400, Message: "请先保存配置"}) + return + } + payload := openaiChatRequest{ + Model: cfg.Model, + Messages: []models.ChatMessage{{Role: "user", Content: "ping"}}, + Temperature: 0, + MaxTokens: 16, + } + body, _ := json.Marshal(payload) + endpoint := resolveEndpoint(cfg.Provider, cfg.Endpoint) + httpReq, _ := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, endpoint+"/chat/completions", bytes.NewReader(body)) + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey) + resp, err := h.client.Do(httpReq) + if err != nil { + c.JSON(http.StatusOK, models.Response{Code: 502, Message: "连接失败: " + err.Error()}) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + raw, _ := io.ReadAll(resp.Body) + msg := fmt.Sprintf("HTTP %d", resp.StatusCode) + if e := parseOpenAIError(raw); e != "" { + msg += ": " + e + } + c.JSON(http.StatusOK, models.Response{Code: resp.StatusCode, Message: msg}) + 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 251b611..b03c308 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -413,6 +413,16 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { agentQuery.POST("/alerts/:id/resolve", taskHandler.ResolveAlert) } + // AI 智能助手 - LLM 配置 + 对话代理(OpenAI 兼容) // Logan.Li + llmHandler := handlers.NewLLMHandler(db) + aiopsGroup := authorized.Group("/aiops") + { + aiopsGroup.GET("/llm-config", llmHandler.GetLLMConfig) + aiopsGroup.PUT("/llm-config", llmHandler.SaveLLMConfig) + aiopsGroup.POST("/llm-config/test", llmHandler.TestLLMConnection) + aiopsGroup.POST("/chat", llmHandler.Chat) + } + // 启动告警引擎(每 30 秒检查一次) alertEngine := handlers.NewAlertEngine(db) go func() { diff --git a/backend/internal/models/llm.go b/backend/internal/models/llm.go new file mode 100644 index 0000000..87d2931 --- /dev/null +++ b/backend/internal/models/llm.go @@ -0,0 +1,52 @@ +package models + +import "time" + +// LLMConfig 大语言模型配置(单行配置,类似 LDAPConfig) // Logan.Li +// 支持 OpenAI 兼容接口(OpenAI / DeepSeek / 通义千问 / Moonshot / Ollama / 自定义) +type LLMConfig struct { + ID uint `gorm:"primarykey" json:"id"` + Provider string `gorm:"size:50;not null;default:openai" json:"provider"` // openai/deepseek/qwen/moonshot/ollama/custom + Model string `gorm:"size:100;not null;default:gpt-4o-mini" json:"model"` // 模型名,如 gpt-4o-mini / deepseek-chat + APIKey string `gorm:"size:500" json:"api_key"` // API Key(GET 时会被脱敏) + Endpoint string `gorm:"size:500" json:"endpoint"` // 基础地址,如 https://api.openai.com/v1 + Temperature float64 `gorm:"default:0.7" json:"temperature"` // 采样温度 0-2 + MaxTokens int `gorm:"default:2000" json:"max_tokens"` // 最大输出 token + SystemPrompt string `gorm:"type:text" json:"system_prompt"` // 系统提示词(AIOps 运维助手角色) + Enabled bool `gorm:"default:false" json:"enabled"` // 是否启用 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// TableName 表名 // Logan.Li +func (LLMConfig) TableName() string { return "llm_configs" } + +// SaveLLMConfigRequest 保存 LLM 配置请求 // Logan.Li +type SaveLLMConfigRequest struct { + Provider string `json:"provider"` + Model string `json:"model"` + APIKey string `json:"api_key"` // 若为脱敏值(含 ****)则后端保留原值 + Endpoint string `json:"endpoint"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"max_tokens"` + SystemPrompt string `json:"system_prompt"` + Enabled bool `json:"enabled"` +} + +// ChatMessage 对话消息 // Logan.Li +type ChatMessage struct { + Role string `json:"role"` // system / user / assistant + Content string `json:"content"` +} + +// ChatRequest AI 对话请求 // Logan.Li +type ChatRequest struct { + Messages []ChatMessage `json:"messages" binding:"required,min=1,dive"` +} + +// ChatResponse AI 对话响应 // Logan.Li +type ChatResponse struct { + Content string `json:"content"` + Model string `json:"model"` + TokensUsed int `json:"tokens_used"` +} diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index 2ad86d7..b909561 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -102,5 +102,6 @@ func AutoMigrate(db *gorm.DB) error { &models.LogEntry{}, &models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li &models.TerminalSession{}, // 终端会话录制(终端审计) // Logan.Li + &models.LLMConfig{}, // 大语言模型配置(AI 智能助手) // Logan.Li ) } diff --git a/frontend/src/components/aiops/AIOpsAssistant.tsx b/frontend/src/components/aiops/AIOpsAssistant.tsx index 9fd9ac1..5dc740b 100644 --- a/frontend/src/components/aiops/AIOpsAssistant.tsx +++ b/frontend/src/components/aiops/AIOpsAssistant.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { @@ -15,7 +14,6 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, @@ -23,116 +21,182 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; +import { aiApi, type LLMConfigData } from '@/services/api'; import { Bot, Send, Settings, - Sparkles, AlertTriangle, - Server, Activity, - TrendingUp, FileText, + TrendingUp, Terminal, RotateCw, - ChevronRight, - Copy, - Check, - Clock, User, MessageSquare, - Zap, Brain, - Cpu, - BarChart3, Loader2, + CheckCircle2, + Wifi, } from 'lucide-react'; +// LLM 提供商列表(OpenAI 兼容接口) // Logan.Li +const LLM_PROVIDERS: { id: string; name: string; defaultEndpoint: string; models: string[] }[] = [ + { id: 'openai', name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1', models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo'] }, + { id: 'deepseek', name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1', models: ['deepseek-chat', 'deepseek-reasoner'] }, + { id: 'qwen', name: '通义千问 (DashScope)', defaultEndpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1', models: ['qwen-plus', 'qwen-turbo', 'qwen-max'] }, + { id: 'moonshot', name: 'Moonshot 月之暗面', defaultEndpoint: 'https://api.moonshot.cn/v1', models: ['moonshot-v1-8k', 'moonshot-v1-32k'] }, + { id: 'ollama', name: 'Ollama 本地', defaultEndpoint: 'http://localhost:11434/v1', models: ['qwen2.5:7b', 'llama3.2', 'deepseek-r1:7b'] }, + { id: 'custom', name: '自定义 (OpenAI 兼容)', defaultEndpoint: '', models: [] }, +]; + +const defaultConfig: LLMConfigData = { + provider: 'openai', + model: 'gpt-4o-mini', + api_key: '', + endpoint: 'https://api.openai.com/v1', + temperature: 0.7, + max_tokens: 2000, + system_prompt: '', + enabled: false, +}; + +// 快捷操作(填入输入框供用户发送) // Logan.Li +const quickActions = [ + { id: 'alerts', name: '分析今日告警', icon: AlertTriangle, prompt: '帮我分析最近的告警事件,给出根因分析和处置建议' }, + { id: 'performance', name: '性能瓶颈分析', icon: Activity, prompt: '分析当前系统的性能瓶颈,找出 CPU/内存/磁盘使用率最高的服务' }, + { id: 'report', name: '生成运维报告', icon: FileText, prompt: '生成今日运维报告,包括资源使用、告警、发布情况' }, + { id: 'optimize', name: '资源优化建议', icon: TrendingUp, prompt: '根据当前资源使用情况提供成本优化和容量规划建议' }, +]; + interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: string; - suggestions?: string[]; + isError?: boolean; } -interface LLMConfig { - provider: string; - model: string; - apiKey: string; - endpoint: string; - temperature: number; - maxTokens: number; +// 格式化时间为 HH:MM // Logan.Li +function fmtTime(iso: string): string { + const d = new Date(iso); + return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); } -// LLM 提供商列表(需后端配置后填充) -const llmProviders: { id: string; name: string; models: string[] }[] = []; - -const quickActions = [ - { id: 'alerts', name: '分析今日告警', icon: AlertTriangle, description: '分析今日所有告警事件' }, - { id: 'performance', name: '性能瓶颈分析', icon: Activity, description: '分析系统性能瓶颈' }, - { id: 'report', name: '生成运维报告', icon: FileText, description: '生成每日运维报告' }, - { id: 'optimize', name: '资源优化建议', icon: TrendingUp, description: '提供资源优化方案' }, -]; - export function AIOpsAssistant() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isConfigOpen, setIsConfigOpen] = useState(false); - const [llmConfig, setLlmConfig] = useState({ - provider: '', - model: '', - apiKey: '', - endpoint: '', - temperature: 0.7, - maxTokens: 2000, - }); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [config, setConfig] = useState(defaultConfig); + const [configReady, setConfigReady] = useState(false); // 已配置且启用 const messagesEndRef = useRef(null); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + // 滚动到底部 // Logan.Li + const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + useEffect(() => { scrollToBottom(); }, [messages]); + + // 加载 LLM 配置 // Logan.Li + const loadConfig = async () => { + try { + const cfg = await aiApi.getConfig(); + setConfig(cfg); + setConfigReady(!!(cfg.enabled && cfg.configured)); + } catch { + setConfigReady(false); + } + }; + useEffect(() => { loadConfig(); }, []); + + // 切换提供商时自动填充默认 endpoint + 模型 // Logan.Li + const handleProviderChange = (provider: string) => { + const p = LLM_PROVIDERS.find((x) => x.id === provider); + setConfig((prev) => ({ + ...prev, + provider, + endpoint: p?.defaultEndpoint || prev.endpoint, + model: p?.models[0] || prev.model, + })); }; - useEffect(() => { - scrollToBottom(); - }, [messages]); + // 保存配置 // Logan.Li + const handleSaveConfig = async () => { + if (config.enabled && !config.api_key && config.provider !== 'ollama') { + toast.error('请填写 API Key'); + return; + } + setSaving(true); + try { + const saved = await aiApi.saveConfig(config); + setConfig(saved); + setConfigReady(!!(saved.enabled && (saved as any).configured)); + toast.success('配置已保存'); + setIsConfigOpen(false); + } catch (e) { + toast.error(e instanceof Error ? e.message : '保存失败'); + } finally { + setSaving(false); + } + }; - const handleSend = () => { - if (!input.trim()) return; + // 测试连接 // Logan.Li + const handleTest = async () => { + setTesting(true); + try { + await aiApi.testConnection(); + toast.success('LLM 服务连接成功'); + } catch (e) { + toast.error(e instanceof Error ? e.message : '连接失败'); + } finally { + setTesting(false); + } + }; - const userMessage: Message = { + // 发送消息(真实 LLM 对话) // Logan.Li + const handleSend = async () => { + const text = input.trim(); + if (!text || isLoading) return; + const userMsg: Message = { id: Date.now().toString(), role: 'user', - content: input, + content: text, timestamp: new Date().toISOString(), }; - - setMessages((prev) => [...prev, userMessage]); + const newMessages = [...messages, userMsg]; + setMessages(newMessages); setInput(''); - - // AI 功能需配置 LLM 接口,暂不返回模拟回复 - toast.info('AI 功能需配置 LLM 接口,请在「模型配置」中完成设置'); - const assistantMessage: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: '当前未配置 LLM 服务,无法提供智能回复。请在右上角「模型配置」中配置 LLM 接口后重试。', - timestamp: new Date().toISOString(), - }; - setMessages((prev) => [...prev, assistantMessage]); + setIsLoading(true); + try { + // 仅保留最近 10 条上下文避免 token 超限 // Logan.Li + const history = newMessages.slice(-10).map((m) => ({ role: m.role, content: m.content })); + const result = await aiApi.chat(history); + setMessages((prev) => [ + ...prev, + { id: (Date.now() + 1).toString(), role: 'assistant', content: result.content, timestamp: new Date().toISOString() }, + ]); + } catch (e) { + const errMsg = e instanceof Error ? e.message : '对话失败'; + setMessages((prev) => [ + ...prev, + { id: (Date.now() + 1).toString(), role: 'assistant', content: `⚠️ ${errMsg}`, timestamp: new Date().toISOString(), isError: true }, + ]); + } finally { + setIsLoading(false); + } }; - const handleQuickAction = (actionId: string) => { - const actionMap: Record = { - alerts: '分析今日告警', - performance: '分析系统性能瓶颈', - report: '生成今日运维报告', - optimize: '提供资源优化建议', - }; - setInput(actionMap[actionId] || ''); + // 新对话 // Logan.Li + const handleNewChat = () => { + setMessages([]); + setInput(''); }; + const currentProvider = LLM_PROVIDERS.find((p) => p.id === config.provider); + return (
{/* Header */} @@ -146,10 +210,22 @@ export function AIOpsAssistant() {

AI 驱动的智能运维助手

- +
+ {configReady ? ( + + + {config.model} + + ) : ( + + 未配置 + + )} + +
@@ -165,13 +241,10 @@ export function AIOpsAssistant() { key={action.id} variant="ghost" className="w-full justify-start text-slate-300 hover:text-slate-100 hover:bg-slate-700" - onClick={() => handleQuickAction(action.id)} + onClick={() => setInput(action.prompt)} > - -
-

{action.name}

-

{action.description}

-
+ + {action.name} ))} @@ -209,12 +282,16 @@ export function AIOpsAssistant() {
AI 助手 - - - 在线 - + {configReady ? ( + + + 在线 + + ) : ( + 离线 + )}
- @@ -226,15 +303,17 @@ export function AIOpsAssistant() { {messages.length === 0 && !isLoading && (
-

暂无对话记录

-

输入问题开始与 AI 助手对话

+

{configReady ? '暂无对话记录,输入问题开始与 AI 助手对话' : 'AI 助手尚未配置大模型'}

+ {!configReady && ( + + )}
)} {messages.map((message) => ( -
+
{message.role === 'assistant' ? : } @@ -242,30 +321,17 @@ export function AIOpsAssistant() {
-
{message.content}
+ {message.content}
-
{message.timestamp}
- {message.suggestions && message.role === 'assistant' && ( -
- {message.suggestions.map((suggestion, idx) => ( - - ))} -
- )} +
{fmtTime(message.timestamp)}
))} @@ -294,10 +360,11 @@ export function AIOpsAssistant() { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} - placeholder="输入问题或选择快速操作..." + placeholder={configReady ? '输入运维问题,或选择左侧快速操作...' : '请先配置大模型...'} className="flex-1 bg-slate-900 border-slate-700 text-slate-100" + disabled={isLoading} /> -
@@ -310,72 +377,84 @@ export function AIOpsAssistant() { 大模型配置 - 配置 AI 助手使用的大语言模型 + + 配置 AI 助手使用的大语言模型(支持 OpenAI 兼容接口) + -
+
+ {/* 启用开关 */} +
+
+

启用 AI 助手

+

关闭后将无法进行对话

+
+ setConfig({ ...config, enabled: v })} /> +
+
- - {llmProviders.map((p) => ( - - {p.name} - + {LLM_PROVIDERS.map((p) => ( + {p.name} ))} - {llmProviders.length === 0 && ( -
暂无可用的 LLM 提供商
- )}
- setConfig({ ...config, model: v })}> + + + + + {currentProvider.models.map((m) => ( + {m} ))} - {!llmProviders.find((p) => p.id === llmConfig.provider)?.models?.length && ( -
请先选择提供商
- )} -
- + + + ) : ( + setConfig({ ...config, model: e.target.value })} + placeholder="输入模型名称" + className="bg-slate-800 border-slate-700 text-slate-100 mt-1" + /> + )}
- + setLlmConfig({ ...llmConfig, apiKey: e.target.value })} + value={config.api_key} + onChange={(e) => setConfig({ ...config, api_key: e.target.value })} placeholder="sk-..." className="bg-slate-800 border-slate-700 text-slate-100 mt-1" />
- + setLlmConfig({ ...llmConfig, endpoint: e.target.value })} + value={config.endpoint} + onChange={(e) => setConfig({ ...config, endpoint: e.target.value })} placeholder="https://api.openai.com/v1" - className="bg-slate-800 border-slate-700 text-slate-100 mt-1" + className="bg-slate-800 border-slate-700 text-slate-100 mt-1 font-mono text-sm" + /> +

基础地址(不含 /chat/completions),OpenAI 兼容格式

+
+
+ +