feat: AIOps智能助手接入真实LLM后端(OpenAI兼容代理+配置持久化)
后端: - models/llm.go: LLMConfig模型(单行配置)+请求/响应struct - handlers/llm.go: GetLLMConfig/SaveLLMConfig(脱敏+upsert)/Chat(OpenAI兼容代理)/TestLLMConnection - routes.go: 注册 /aiops/llm-config(GET/PUT/test)+/aiops/chat(POST) - database.go: AutoMigrate LLMConfig - 支持OpenAI/DeepSeek/通义千问/Moonshot/Ollama/自定义6种提供商 - API Key脱敏(仅保留后4位)+回传脱敏值保留原值 - 优雅错误处理(连接超时/401/未配置均返回友好消息) - 120s超时+AIOps运维助手默认系统提示词 前端: - AIOpsAssistant.tsx: 全量重写接入真实后端 · 配置弹窗从后端加载(持久化),提供商切换自动填充endpoint/模型 · 启用开关+测试连接按钮+保存反馈 · 真实对话(发送最近10条上下文),思考中spinner,错误红色气泡 · 状态徽章(未配置/在线+模型名),新对话清空 - api.ts: aiApi替代mock aiAssistantApi(getConfig/saveConfig/testConnection/chat) 验证: tsc零错误+build通过+/health正常+前后端联调通过(配置CRUD+API Key脱敏+masked-key保留+chat错误优雅降级) // Logan.Li
This commit is contained in:
1 parent
2e174fe7eb
commit
b8cab7ed0f
6 files changed
+627
-176
No files matched your search
@@ -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<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
const [llmConfig, setLlmConfig] = useState<LLMConfig>({
|
||||
provider: '',
|
||||
model: '',
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
temperature: 0.7,
|
||||
maxTokens: 2000,
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [config, setConfig] = useState<LLMConfigData>(defaultConfig);
|
||||
const [configReady, setConfigReady] = useState(false); // 已配置且启用
|
||||
const messagesEndRef = useRef<HTMLDivElement>(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<string, string> = {
|
||||
alerts: '分析今日告警',
|
||||
performance: '分析系统性能瓶颈',
|
||||
report: '生成今日运维报告',
|
||||
optimize: '提供资源优化建议',
|
||||
};
|
||||
setInput(actionMap[actionId] || '');
|
||||
// 新对话 // Logan.Li
|
||||
const handleNewChat = () => {
|
||||
setMessages([]);
|
||||
setInput('');
|
||||
};
|
||||
|
||||
const currentProvider = LLM_PROVIDERS.find((p) => p.id === config.provider);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -146,10 +210,22 @@ export function AIOpsAssistant() {
|
||||
<p className="text-sm text-slate-500">AI 驱动的智能运维助手</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setIsConfigOpen(true)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
模型配置
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
{configReady ? (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
{config.model}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-amber-500/30 text-amber-400 bg-amber-500/10">
|
||||
未配置
|
||||
</Badge>
|
||||
)}
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setIsConfigOpen(true)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
模型配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-6 h-[calc(100vh-200px)]">
|
||||
@@ -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.icon className="h-4 w-4 mr-2" />
|
||||
<div className="text-left">
|
||||
<p className="text-sm">{action.name}</p>
|
||||
<p className="text-xs text-slate-500">{action.description}</p>
|
||||
</div>
|
||||
<action.icon className="h-4 w-4 mr-2 shrink-0" />
|
||||
<span className="text-sm">{action.name}</span>
|
||||
</Button>
|
||||
))}
|
||||
</CardContent>
|
||||
@@ -209,12 +282,16 @@ export function AIOpsAssistant() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="h-5 w-5 text-purple-400" />
|
||||
<span className="font-medium text-slate-100">AI 助手</span>
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10 text-xs">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 mr-1 animate-pulse" />
|
||||
在线
|
||||
</Badge>
|
||||
{configReady ? (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10 text-xs">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 mr-1 animate-pulse" />
|
||||
在线
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-slate-600 text-slate-400 text-xs">离线</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-slate-400">
|
||||
<Button variant="ghost" size="sm" className="text-slate-400" onClick={handleNewChat} disabled={messages.length === 0}>
|
||||
<RotateCw className="h-4 w-4 mr-1" />
|
||||
新对话
|
||||
</Button>
|
||||
@@ -226,15 +303,17 @@ export function AIOpsAssistant() {
|
||||
{messages.length === 0 && !isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
|
||||
<MessageSquare className="h-10 w-10 mb-3 opacity-30" />
|
||||
<p>暂无对话记录</p>
|
||||
<p className="text-xs mt-1">输入问题开始与 AI 助手对话</p>
|
||||
<p>{configReady ? '暂无对话记录,输入问题开始与 AI 助手对话' : 'AI 助手尚未配置大模型'}</p>
|
||||
{!configReady && (
|
||||
<Button variant="outline" size="sm" className="mt-4 border-slate-600 text-slate-300" onClick={() => setIsConfigOpen(true)}>
|
||||
<Settings className="h-4 w-4 mr-1" />
|
||||
前往配置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-3 ${message.role === 'user' ? 'flex-row-reverse' : ''}`}
|
||||
>
|
||||
<div key={message.id} className={`flex gap-3 ${message.role === 'user' ? 'flex-row-reverse' : ''}`}>
|
||||
<Avatar className={message.role === 'assistant' ? 'bg-purple-500' : 'bg-blue-500'}>
|
||||
<AvatarFallback>
|
||||
{message.role === 'assistant' ? <Bot className="h-4 w-4" /> : <User className="h-4 w-4" />}
|
||||
@@ -242,30 +321,17 @@ export function AIOpsAssistant() {
|
||||
</Avatar>
|
||||
<div className={`max-w-[80%] ${message.role === 'user' ? 'text-right' : ''}`}>
|
||||
<div
|
||||
className={`inline-block rounded-lg p-3 text-left ${
|
||||
message.role === 'assistant'
|
||||
? 'bg-slate-700 text-slate-100'
|
||||
: 'bg-blue-600 text-white'
|
||||
className={`inline-block rounded-lg p-3 text-left whitespace-pre-wrap break-words ${
|
||||
message.isError
|
||||
? 'bg-red-900/30 border border-red-500/30 text-red-300'
|
||||
: message.role === 'assistant'
|
||||
? 'bg-slate-700 text-slate-100'
|
||||
: 'bg-blue-600 text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||
{message.content}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{message.timestamp}</div>
|
||||
{message.suggestions && message.role === 'assistant' && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
{message.suggestions.map((suggestion, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-slate-600 text-slate-300 text-xs"
|
||||
onClick={() => setInput(suggestion)}
|
||||
>
|
||||
{suggestion}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-slate-500 mt-1">{fmtTime(message.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -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}
|
||||
/>
|
||||
<Button className="bg-purple-600 hover:bg-purple-700" onClick={handleSend} disabled={isLoading}>
|
||||
<Button className="bg-purple-600 hover:bg-purple-700" onClick={handleSend} disabled={isLoading || !input.trim()}>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -310,72 +377,84 @@ export function AIOpsAssistant() {
|
||||
<DialogContent className="max-w-2xl bg-slate-900 border-slate-700 text-slate-100">
|
||||
<DialogHeader>
|
||||
<DialogTitle>大模型配置</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">配置 AI 助手使用的大语言模型</DialogDescription>
|
||||
<DialogDescription className="text-slate-500">
|
||||
配置 AI 助手使用的大语言模型(支持 OpenAI 兼容接口)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto pr-1">
|
||||
{/* 启用开关 */}
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-700 p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">启用 AI 助手</p>
|
||||
<p className="text-xs text-slate-500">关闭后将无法进行对话</p>
|
||||
</div>
|
||||
<Switch checked={config.enabled} onCheckedChange={(v) => setConfig({ ...config, enabled: v })} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-slate-300">提供商</Label>
|
||||
<Select
|
||||
value={llmConfig.provider}
|
||||
onValueChange={(v) => setLlmConfig({ ...llmConfig, provider: v })}
|
||||
>
|
||||
<Select value={config.provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger className="bg-slate-800 border-slate-700 text-slate-100 mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{llmProviders.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id} className="text-slate-100">
|
||||
{p.name}
|
||||
</SelectItem>
|
||||
{LLM_PROVIDERS.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id} className="text-slate-100">{p.name}</SelectItem>
|
||||
))}
|
||||
{llmProviders.length === 0 && (
|
||||
<div className="px-3 py-2 text-sm text-slate-500">暂无可用的 LLM 提供商</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">模型</Label>
|
||||
<Select
|
||||
value={llmConfig.model}
|
||||
onValueChange={(v) => setLlmConfig({ ...llmConfig, model: v })}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-800 border-slate-700 text-slate-100 mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{llmProviders
|
||||
.find((p) => p.id === llmConfig.provider)
|
||||
?.models.map((m) => (
|
||||
<SelectItem key={m} value={m} className="text-slate-100">
|
||||
{m}
|
||||
</SelectItem>
|
||||
{currentProvider && currentProvider.models.length > 0 ? (
|
||||
<Select value={config.model} onValueChange={(v) => setConfig({ ...config, model: v })}>
|
||||
<SelectTrigger className="bg-slate-800 border-slate-700 text-slate-100 mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{currentProvider.models.map((m) => (
|
||||
<SelectItem key={m} value={m} className="text-slate-100">{m}</SelectItem>
|
||||
))}
|
||||
{!llmProviders.find((p) => p.id === llmConfig.provider)?.models?.length && (
|
||||
<div className="px-3 py-2 text-sm text-slate-500">请先选择提供商</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={config.model}
|
||||
onChange={(e) => setConfig({ ...config, model: e.target.value })}
|
||||
placeholder="输入模型名称"
|
||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">API Key</Label>
|
||||
<Label className="text-slate-300">API Key {config.provider === 'ollama' && <span className="text-xs text-slate-500">(本地 Ollama 通常无需)</span>}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={llmConfig.apiKey}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">API Endpoint (可选)</Label>
|
||||
<Label className="text-slate-300">API Endpoint</Label>
|
||||
<Input
|
||||
value={llmConfig.endpoint}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">基础地址(不含 /chat/completions),OpenAI 兼容格式</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">系统提示词(可选)</Label>
|
||||
<textarea
|
||||
value={config.system_prompt}
|
||||
onChange={(e) => setConfig({ ...config, system_prompt: e.target.value })}
|
||||
placeholder="留空则使用内置 AIOps 运维助手角色提示词"
|
||||
className="w-full bg-slate-800 border border-slate-700 text-slate-100 mt-1 rounded-md p-2 text-sm min-h-[70px] resize-y"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -386,28 +465,33 @@ export function AIOpsAssistant() {
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={llmConfig.temperature}
|
||||
onChange={(e) => setLlmConfig({ ...llmConfig, temperature: parseFloat(e.target.value) })}
|
||||
value={Math.round(config.temperature * 10) / 10}
|
||||
onChange={(e) => setConfig({ ...config, temperature: parseFloat(e.target.value) || 0 })}
|
||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">值越高,回答越创造性</p>
|
||||
<p className="text-xs text-slate-500 mt-1">值越高回答越创造性</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">Max Tokens</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={llmConfig.maxTokens}
|
||||
onChange={(e) => setLlmConfig({ ...llmConfig, maxTokens: parseInt(e.target.value) })}
|
||||
value={config.max_tokens}
|
||||
onChange={(e) => setConfig({ ...config, max_tokens: parseInt(e.target.value) || 2000 })}
|
||||
className="bg-slate-800 border-slate-700 text-slate-100 mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={handleTest} disabled={testing} className="border-slate-700 text-slate-300">
|
||||
{testing ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Wifi className="h-4 w-4 mr-2" />}
|
||||
测试连接
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsConfigOpen(false)} className="border-slate-700 text-slate-300">
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-purple-600 hover:bg-purple-700" onClick={() => setIsConfigOpen(false)}>
|
||||
<Button className="bg-purple-600 hover:bg-purple-700" onClick={handleSaveConfig} disabled={saving}>
|
||||
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
|
||||
保存配置
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -634,23 +634,33 @@ export const dashboardApi = {
|
||||
createDatasource: async (_data: any) => ({ success: false }),
|
||||
};
|
||||
|
||||
// AI Assistant API(无后端)
|
||||
export const aiAssistantApi = {
|
||||
chat: async (message: string, history: any[]) => {
|
||||
return {
|
||||
data: {
|
||||
reply: `我收到了您的问题: "${message}"。作为AIOps智能助手,我可以帮您分析告警、诊断问题、生成报告等。`,
|
||||
suggestions: ['分析当前告警', '查看性能趋势', '生成运维报告'],
|
||||
},
|
||||
};
|
||||
},
|
||||
analyzeAlert: async (alertId: string) => {
|
||||
return {
|
||||
data: {
|
||||
analysis: '根据告警信息分析,可能是由于数据库连接池耗尽导致的。建议检查数据库连接配置和当前连接数。',
|
||||
recommendations: ['增加连接池大小', '检查慢查询', '重启应用服务'],
|
||||
},
|
||||
};
|
||||
},
|
||||
generateReport: async (params: any) => ({ data: { report: '运维报告已生成...' } }),
|
||||
// AI 智能助手 API(后端 LLM 配置 + 对话代理,OpenAI 兼容) // Logan.Li
|
||||
export interface LLMConfigData {
|
||||
configured?: boolean;
|
||||
provider: string;
|
||||
model: string;
|
||||
api_key: string;
|
||||
endpoint: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
system_prompt: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface ChatResult {
|
||||
content: string;
|
||||
model: string;
|
||||
tokens_used: number;
|
||||
}
|
||||
|
||||
export const aiApi = {
|
||||
// 获取 LLM 配置(API Key 已脱敏)
|
||||
getConfig: () => http.get<LLMConfigData>('/aiops/llm-config'),
|
||||
// 保存 LLM 配置(api_key 若为脱敏值则后端保留原值)
|
||||
saveConfig: (data: LLMConfigData) => http.put<LLMConfigData>('/aiops/llm-config', data),
|
||||
// 测试 LLM 连接(发送 ping)
|
||||
testConnection: () => http.post('/aiops/llm-config/test'),
|
||||
// AI 对话(messages 含历史 + 当前用户消息)
|
||||
chat: (messages: { role: 'user' | 'assistant'; content: string }[]) =>
|
||||
http.post<ChatResult>('/aiops/chat', { messages }),
|
||||
};
|
||||
Reference in new issue
Block a user