feat(log-center): v0.96 日志库/查询页数据源打通 + AgentCLI 输入修复 + AI助手按钮去重

This commit is contained in:
attacker committed 2026-06-27 14:07:11 +08:00
1 parent ba8a4b5f19
commit 8dd5a07b70
4 files changed
+53 -32

No files matched your search

@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -145,6 +146,7 @@ function fmtTime(iso: string): string {
}
export function AIOpsAssistant() {
const navigate = useNavigate();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -381,20 +383,12 @@ export function AIOpsAssistant() {
</div>
</div>
<div className="flex items-center gap-3">
{configReady ? (
{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>
@@ -483,7 +477,12 @@ export function AIOpsAssistant() {
<MessageSquare className="h-10 w-10 mb-3 opacity-30" />
<p>{configReady ? '暂无对话记录,输入问题开始与 AI 助手对话' : 'AI 助手尚未配置大模型'}</p>
{!configReady && (
<Button variant="outline" size="sm" className="mt-4 border-slate-600 text-slate-300" onClick={() => setIsConfigOpen(true)}>
<Button
variant="outline"
size="sm"
className="mt-4 border-slate-700 text-slate-300 hover:text-purple-400"
onClick={() => navigate('/ai-llm-config')}
>
<Settings className="h-4 w-4 mr-1" />
前往配置
</Button>
+28 -13
View File
@@ -2,7 +2,6 @@
// 关联主机后,用自然语言描述任务,AI 通过 SSH 工具自主排障/部署,循环重试直到解决。
// 通过 WebSocket 实时展示思考、执行的命令、输出与结果;高危命令弹框确认。
import { useState, useRef, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -12,8 +11,9 @@ import {
} from '@/components/ui/select';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { serverApi, buildAgentCliWsUrl } from '@/api';
import { toast } from 'sonner';
import {
Send, Square, Terminal, Settings, ChevronDown, ChevronRight,
Send, Square, Terminal, ChevronDown, ChevronRight,
Shield, Brain, Cpu, Database, Loader2, Bot, AlertTriangle, CheckCircle2, Lightbulb, Server,
} from 'lucide-react';
@@ -57,7 +57,6 @@ function riskClass(risk?: string): string {
}
export default function AgentCLI() {
const navigate = useNavigate();
const confirm = useConfirm();
const [servers, setServers] = useState<{ id: number; hostname: string; ip: string }[]>([]);
@@ -67,7 +66,7 @@ export default function AgentCLI() {
const [running, setRunning] = useState(false);
const [round, setRound] = useState(0);
const [tokens, setTokens] = useState(0);
const [tipsOpen, setTipsOpen] = useState(true);
const [tipsOpen, setTipsOpen] = useState(false);
const [connected, setConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
@@ -95,6 +94,7 @@ export default function AgentCLI() {
wsRef.current = ws;
ws.onopen = () => setConnected(true);
ws.onclose = () => { setConnected(false); setRunning(false); };
ws.onerror = () => { setConnected(false); };
ws.onmessage = (e) => {
try { handleEvent(JSON.parse(e.data)); } catch { /* 忽略非 JSON */ }
};
@@ -176,7 +176,13 @@ export default function AgentCLI() {
// 发送任务 // Logan.Li
const send = () => {
const q = input.trim();
if (!q || running || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
if (!q) return;
if (!serverId) { toast.error('请先选择目标主机'); return; }
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
toast.error('连接未就绪,请检查大模型配置或刷新页面重试');
return;
}
if (running) return;
push({ kind: 'user', content: q });
wsRef.current.send(JSON.stringify({ type: 'start', query: q }));
setInput('');
@@ -219,9 +225,6 @@ export default function AgentCLI() {
</Badge>
)}
<div className="flex-1" />
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => navigate('/ai-llm-config')}>
<Settings className="w-4 h-4 mr-1" /> 大模型设置
</Button>
</CardContent>
</Card>
@@ -232,7 +235,6 @@ export default function AgentCLI() {
{tipsOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
<Lightbulb className="w-4 h-4 text-yellow-400" />
<span className="font-medium">核心能力与设计思路</span>
<span className="text-slate-500 text-xs">— Agent CLI 由四大模块构成,参考 opsxcli AI Agent 移植</span>
</div>
</CardHeader>
{tipsOpen && (
@@ -254,9 +256,23 @@ export default function AgentCLI() {
<Card className="flex-1 min-h-0 bg-slate-900/40 border-slate-800 flex flex-col">
<div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto p-4 space-y-3">
{items.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-slate-500 gap-2">
<div className="h-full flex flex-col items-center justify-center text-slate-500 gap-3">
<Terminal className="w-10 h-10 opacity-40" />
<div className="text-sm">{serverId ? '描述你要执行的任务,例如「检查磁盘使用率并清理大文件」' : '请先选择一台目标主机'}</div>
{!serverId ? (
<>
<div className="text-sm">请先选择一台目标主机</div>
{servers.length === 0 && (
<div className="text-xs text-slate-600">暂无主机,请先前往「资产管理」添加服务器</div>
)}
</>
) : !connected ? (
<>
<Loader2 className="w-6 h-6 animate-spin text-blue-400" />
<div className="text-sm">正在连接主机...</div>
</>
) : (
<div className="text-sm">描述你要执行的任务,例如「检查磁盘使用率并清理大文件」</div>
)}
</div>
)}
{items.map((it) => (
@@ -270,7 +286,6 @@ export default function AgentCLI() {
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
placeholder={serverId ? '输入任务描述,回车发送(Shift+回车换行)' : '请先选择目标主机'}
disabled={!connected}
rows={1}
className="flex-1 resize-none bg-slate-800 border border-slate-700 rounded-md px-3 py-2 text-sm text-slate-200 placeholder:text-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500 max-h-32"
/>
@@ -279,7 +294,7 @@ export default function AgentCLI() {
<Square className="w-4 h-4 mr-1" /> 停止
</Button>
) : (
<Button onClick={send} disabled={!connected || !input.trim()} className="bg-blue-600 hover:bg-blue-700">
<Button onClick={send} disabled={!input.trim()} className="bg-blue-600 hover:bg-blue-700">
<Send className="w-4 h-4 mr-1" /> 执行
</Button>
)}
@@ -4,7 +4,7 @@ import { Navigate, useParams } from 'react-router-dom';
import { ScrollText } from 'lucide-react';
import { LogQueryProvider } from './LogQueryContext';
import { UnifiedQueryPanel } from './UnifiedQueryPanel';
import { LogLibraryPage } from './LogLibraryPage';
import { LogDatasourcePage } from './LogDatasourcePage';
import { LogAlertsPanel } from './LogAlertsPanel';
import type { LogEngine } from './types-v3';
@@ -42,7 +42,7 @@ export default function LogCenter() {
{(tab === 'loki' || tab === 'es') && meta.engine && (
<UnifiedQueryPanel engine={meta.engine} onRuleCreated={() => setAlertReloadKey(k => k + 1)} />
)}
{tab === 'datasources' && <LogLibraryPage />}
{tab === 'datasources' && <LogDatasourcePage />}
{tab === 'alerts' && <LogAlertsPanel key={alertReloadKey} />}
</div>
</LogQueryProvider>
@@ -53,19 +53,26 @@ export const STATUS_CONFIG: Record<string, StatusConfigItem> = {
untested: { color: 'var(--dg-text-muted)', label: '未测试', dot: '#94a3b8' },
};
/** Mock 数据源 */
/** Mock 数据源(仅演示用 — demo-loki / demo-es 各一个) */
export const MOCK_DATASOURCES: LogDatasource[] = [
{ id: 1, engine: 'loki', name: 'prod-loki', url: 'http://loki.prod.local:3100', status: 'connected', description: '生产环境 Loki', isDefault: true, auth: null },
{ id: 2, engine: 'loki', name: 'dev-loki', url: 'http://loki.dev.local:3100', status: 'connected', description: '开发环境 Loki', isDefault: false, auth: null },
{ id: 3, engine: 'elasticsearch', name: 'prod-es', url: 'http://es.prod.local:9200', status: 'connected', description: '生产环境 ES', isDefault: true, auth: null },
{ id: 4, engine: 'elasticsearch', name: 'dev-es', url: 'http://es.dev.local:9200', status: 'error', description: '开发环境 ES', isDefault: false, auth: null },
{ id: 1, engine: 'loki', name: 'demo-loki', url: '', status: 'connected', description: 'Loki 模拟数据', isDefault: true, auth: null },
{ id: 2, engine: 'elasticsearch', name: 'demo-es', url: '', status: 'connected', description: 'ES 模拟数据', isDefault: true, auth: null },
];
/** 从 localStorage 加载数据源 */
/** 从 localStorage 加载数据源(迁移旧数据自动清理) */
export function loadDatasources(): LogDatasource[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
if (raw) {
const list: LogDatasource[] = JSON.parse(raw);
// 版本迁移:旧数据(含 prod-loki / dev-loki 等)强制重置为 demo
const oldNames = list.map((d) => d.name);
if (oldNames.includes('prod-loki') || oldNames.includes('dev-es') || oldNames.includes('prod-es') || oldNames.includes('dev-loki')) {
localStorage.removeItem(STORAGE_KEY);
return [...MOCK_DATASOURCES];
}
return list;
}
} catch { /* ignore */ }
return [...MOCK_DATASOURCES];
}