Files
evotraders/frontend/src/components/RuntimeView.jsx
cillin 16b54d5ccc feat(agent): complete EvoAgent integration for all 6 agent roles
Migrate all agent roles from Legacy to EvoAgent architecture:
- fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst
- risk_manager, portfolio_manager

Key changes:
- EvoAgent now supports Portfolio Manager compatibility methods (_make_decision,
  get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio)
- Add UnifiedAgentFactory for centralized agent creation
- ToolGuard with batch approval API and WebSocket broadcast
- Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent)
- Remove backend/agents/compat.py migration shim
- Add run_id alongside workspace_id for semantic clarity
- Complete integration test coverage (13 tests)
- All smoke tests passing for 6 agent roles

Constraint: Must maintain backward compatibility with existing run configs
Constraint: Memory support must work with EvoAgent (no fallback to Legacy)
Rejected: Separate PM implementation for EvoAgent | unified approach cleaner
Confidence: high
Scope-risk: broad
Directive: EVO_AGENT_IDS env var still respected but defaults to all roles
Not-tested: Kubernetes sandbox mode for skill execution
2026-04-02 00:55:08 +08:00

813 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import {
approvePendingApproval,
denyPendingApproval,
loadAllRuntimeState
} from '../services/runtimeApi';
const AUTO_REFRESH_MS = 5000;
const STATUS_LABELS = {
idle: '空闲',
registered: '已注册',
initializing: '初始化中',
ready: '就绪',
running: '运行中',
analysis_in_progress: '分析中',
risk_review_in_progress: '风控处理中',
discussion_in_progress: '会商中',
decision_in_progress: '决策中',
execution_in_progress: '执行中',
settlement_in_progress: '结算中',
reflection_in_progress: '复盘中',
waiting_approval: '等待审批',
approved: '已批准',
denied: '已拒绝',
completed: '已完成',
error: '异常',
stopped: '已停止'
};
const EVENT_FILTER_OPTIONS = [
{ value: 'all', label: '全部事件' },
{ value: 'cycle', label: '运行周期' },
{ value: 'approval', label: '审批事件' }
];
const SR_ONLY_STYLE = {
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0
};
function metricCard(label, value, accent, helper = null) {
return (
<div className="stat-card">
<div className="stat-card-label">
{label}
</div>
<div className="stat-card-value" style={{ color: accent }}>
{value}
</div>
{helper && (
<div style={{ marginTop: 8, fontSize: 11, color: '#666666', lineHeight: 1.5 }}>
{helper}
</div>
)}
</div>
);
}
function resolveApprovalTone(approval) {
const findings = Array.isArray(approval.findings) ? approval.findings : [];
const levels = findings.map((item) => item?.severity).filter(Boolean);
if (levels.includes('critical')) {
return { border: '#7F1D1D', bg: '#FEF2F2', text: '#991B1B', badgeBg: '#FECACA' };
}
if (levels.includes('high')) {
return { border: '#9A3412', bg: '#FFF7ED', text: '#C2410C', badgeBg: '#FED7AA' };
}
if (levels.includes('medium')) {
return { border: '#92400E', bg: '#FFFBEB', text: '#B45309', badgeBg: '#FDE68A' };
}
return { border: '#D1D5DB', bg: '#FCFCFC', text: '#374151', badgeBg: '#E5E7EB' };
}
// 评估指标配置
const METRICS_CONFIG = {
hit_rate: {
label: '命中率',
icon: '◎',
goodThreshold: 0.7,
warnThreshold: 0.5
},
risk_violation: {
label: '风控违例',
icon: '⚠',
goodThreshold: 0.1,
warnThreshold: 0.3,
inverted: true // 值越小越好
},
decision_latency: {
label: '决策延迟',
icon: '◷',
goodThreshold: 5000,
warnThreshold: 10000,
inverted: true,
unit: 'ms'
},
signal_consistency: {
label: '信号一致性',
icon: '≡',
goodThreshold: 0.8,
warnThreshold: 0.6
}
};
function getMetricColor(value, config) {
if (value === null || value === undefined || isNaN(value)) {
return { color: '#9CA3AF', bg: '#F9FAFB', arrow: '-' };
}
const isInverted = config.inverted;
const effectiveValue = isInverted ? value : value;
const effectiveGood = isInverted ? config.goodThreshold : config.goodThreshold;
const effectiveWarn = isInverted ? config.warnThreshold : config.warnThreshold;
if (effectiveValue <= effectiveGood) {
return { color: '#059669', bg: '#ECFDF5', arrow: '↑' };
} else if (effectiveValue <= effectiveWarn) {
return { color: '#D97706', bg: '#FFFBEB', arrow: '→' };
} else {
return { color: '#DC2626', bg: '#FEF2F2', arrow: '↓' };
}
}
function MetricBadge({ metricKey, value }) {
const config = METRICS_CONFIG[metricKey];
if (!config) return null;
const displayValue = value !== null && value !== undefined && !isNaN(value)
? (config.unit === 'ms' ? `${Math.round(value)}${config.unit}` : `${(value * 100).toFixed(1)}%`)
: '-';
const { color, bg, arrow } = getMetricColor(value, config);
return (
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
padding: '2px 6px',
background: bg,
border: `1px solid ${color}`,
borderRadius: 4,
fontSize: 10,
fontWeight: 600,
color: color
}}>
<span>{config.icon}</span>
<span>{displayValue}</span>
<span style={{ marginLeft: 2 }}>{arrow}</span>
</div>
);
}
function AgentMetricsPanel({ agent }) {
const extensions = agent.extensions || {};
const metrics = [
{ key: 'hit_rate', value: extensions.hit_rate },
{ key: 'risk_violation', value: extensions.risk_violation },
{ key: 'decision_latency', value: extensions.decision_latency },
{ key: 'signal_consistency', value: extensions.signal_consistency }
];
const hasMetrics = metrics.some(m => m.value !== null && m.value !== undefined && !isNaN(m.value));
if (!hasMetrics) return null;
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 6,
marginTop: 8,
paddingTop: 8,
borderTop: '1px dashed #E5E7EB'
}}>
{metrics.map(({ key, value }) => (
<MetricBadge key={key} metricKey={key} value={value} />
))}
</div>
);
}
function sectionTitle(label, action = null) {
return (
<div className="section-header" style={{ marginBottom: 0 }}>
<div className="section-title" style={{ fontSize: 14 }}>
{label}
</div>
{action}
</div>
);
}
function formatStatusLabel(status) {
if (!status) {
return '-';
}
return STATUS_LABELS[status] || status.replace(/_/g, ' ');
}
function formatSessionLabel(sessionId) {
return sessionId || '无会话';
}
function formatApprovalScopeLabel(approval) {
const runId = approval?.run_id || approval?.workspace_id || '-';
const agentId = approval?.agent_id || '-';
return `${agentId} · 运行 ${runId} · ${formatSessionLabel(approval?.session_id)}`;
}
function formatEventLabel(eventName) {
if (!eventName) {
return '-';
}
const [group, action] = String(eventName).split(':');
if (group === 'cycle') {
if (action === 'start') return '周期开始';
if (action === 'complete') return '周期完成';
if (action === 'error') return '周期异常';
return '运行周期';
}
if (group === 'approval') {
if (action === 'created') return '创建审批';
if (action === 'approved') return '审批通过';
if (action === 'denied') return '审批拒绝';
if (action === 'expired') return '审批超时';
return '审批事件';
}
if (group === 'agent') {
if (action === 'status') return '状态更新';
if (action === 'registered') return '注册 Agent';
return 'Agent 事件';
}
return String(eventName).replace(/_/g, ' ');
}
export default function RuntimeView() {
const [runtimeState, setRuntimeState] = useState(null);
const [runtimeError, setRuntimeError] = useState(null);
const [isRuntimeLoading, setIsRuntimeLoading] = useState(false);
const [approvalActionId, setApprovalActionId] = useState(null);
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true);
const [eventFilter, setEventFilter] = useState('all');
const refreshRuntimeState = () => {
setIsRuntimeLoading(true);
loadAllRuntimeState(
(state) => {
setRuntimeState(state);
setRuntimeError(null);
setIsRuntimeLoading(false);
},
(error) => {
setRuntimeError(error.message || '无法加载运行状态');
setIsRuntimeLoading(false);
}
);
};
useEffect(() => {
refreshRuntimeState();
}, []);
useEffect(() => {
if (!autoRefreshEnabled) {
return undefined;
}
const timer = window.setInterval(() => {
refreshRuntimeState();
}, AUTO_REFRESH_MS);
return () => window.clearInterval(timer);
}, [autoRefreshEnabled]);
const handleApprovalAction = async (approvalId, action) => {
setApprovalActionId(approvalId);
try {
if (action === 'approve') {
await approvePendingApproval(approvalId);
} else {
await denyPendingApproval(approvalId);
}
refreshRuntimeState();
} catch (error) {
setRuntimeError(error.message || '审批操作失败');
setIsRuntimeLoading(false);
} finally {
setApprovalActionId(null);
}
};
const agents = runtimeState?.agents || [];
const approvals = runtimeState?.approvals || [];
const events = runtimeState?.events || [];
const activeAgentsCount = agents.filter((agent) => agent.status && agent.status !== 'idle').length;
const visibleEvents = events
.filter((event) => eventFilter === 'all' || event.event.startsWith(eventFilter))
.slice()
.reverse();
return (
<div className="performance-page" style={{ height: '100%', minHeight: 0 }}>
<div className="section">
<div className="section-header">
<div>
<div className="section-title" style={{ fontSize: 18 }}>
运行态控制台
</div>
<div style={{
fontSize: 12,
color: '#666666',
marginTop: 4,
maxWidth: 760,
lineHeight: 1.5
}}>
查看当前运行上下文分析师状态待审批请求与近期事件这里是监控面板不再和运行设置挤在同一个小弹层里
</div>
</div>
<button
onClick={refreshRuntimeState}
disabled={isRuntimeLoading}
style={{
padding: '10px 14px',
borderRadius: 6,
border: '1px solid #111111',
background: isRuntimeLoading ? '#8A8A8A' : '#111111',
color: '#FFFFFF',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.4px',
cursor: isRuntimeLoading ? 'not-allowed' : 'pointer',
whiteSpace: 'nowrap'
}}
>
{isRuntimeLoading ? '刷新中' : '刷新运行态'}
</button>
</div>
</div>
<div className="section">
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
{metricCard('活跃 Agent', activeAgentsCount, '#2563EB', `${agents.length} 个 agent 已注册`)}
{metricCard('待审批', approvals.length, approvals.length > 0 ? '#C2410C' : '#059669', approvals.length > 0 ? '需要人工处理' : '当前无待处理审批')}
{metricCard('运行事件', events.length, '#111111', '最近运行阶段和状态变化')}
<div className="stat-card">
<div className="stat-card-label">
自动刷新
</div>
<button
onClick={() => setAutoRefreshEnabled((value) => !value)}
style={{
padding: '10px 12px',
border: '1px solid #000000',
background: autoRefreshEnabled ? '#000000' : '#FFFFFF',
color: autoRefreshEnabled ? '#FFFFFF' : '#000000',
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.5px',
textTransform: 'uppercase',
cursor: 'pointer'
}}
>
{autoRefreshEnabled ? `开启 / ${AUTO_REFRESH_MS / 1000}` : '关闭'}
</button>
</div>
</div>
</div>
{runtimeError && (
<div className="section" style={{
borderColor: '#FF1744',
background: '#FFF5F7',
color: '#B91C1C',
fontSize: 12,
fontWeight: 700
}}>
{runtimeError}
</div>
)}
<div style={{
display: 'grid',
gap: 20,
alignContent: 'start'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: 'minmax(320px, 0.95fr) minmax(360px, 1.25fr)',
gap: 20,
alignItems: 'start'
}}>
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
{sectionTitle('运行上下文')}
{runtimeState?.context ? (
<div style={{
border: '1px solid #000000',
background: '#FAFAFA',
padding: 12,
display: 'grid',
gap: 10
}}>
<div>
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase' }}>配置名</div>
<div style={{ fontSize: 18, color: '#111111', fontWeight: 800, marginTop: 3 }}>
{runtimeState.context.config_name}
</div>
</div>
<div>
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase' }}>运行目录</div>
<div style={{ fontSize: 11, color: '#111111', lineHeight: 1.5, marginTop: 3, wordBreak: 'break-all' }}>
{runtimeState.context.run_dir}
</div>
</div>
<div>
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase' }}>启动参数</div>
<pre style={{
margin: '6px 0 0',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontSize: 11,
lineHeight: 1.7,
color: '#111111',
fontFamily: '"Courier New", monospace'
}}>
{JSON.stringify(runtimeState.context.bootstrap_values || {}, null, 2)}
</pre>
</div>
</div>
) : (
<div style={{ fontSize: 12, color: '#9CA3AF' }}>暂无运行上下文</div>
)}
</section>
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
{sectionTitle('团队协作状态')}
<div style={{
border: '1px solid #000000',
background: '#FAFAFA',
padding: 12,
display: 'grid',
gap: 12
}}>
{/* 自动广播状态 */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 14 }}>&#128227;</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111111' }}>自动广播</span>
</div>
<span style={{
padding: '4px 10px',
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase',
border: '1px solid #000000',
background: (runtimeState?.context?.auto_broadcast || runtimeState?.context?.team_config?.auto_broadcast)
? '#000000'
: '#FFFFFF',
color: (runtimeState?.context?.auto_broadcast || runtimeState?.context?.team_config?.auto_broadcast)
? '#FFFFFF'
: '#000000',
letterSpacing: '0.5px'
}}>
{(runtimeState?.context?.auto_broadcast || runtimeState?.context?.team_config?.auto_broadcast) ? '已启用' : '已关闭'}
</span>
</div>
{/* Fan-out Pipeline */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 14 }}>&#128101;</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111111' }}>Fan-out Pipeline</span>
</div>
<span style={{
padding: '4px 10px',
fontSize: 11,
fontWeight: 700,
textTransform: 'uppercase',
border: '1px solid #2563EB',
background: '#EFF6FF',
color: '#2563EB',
letterSpacing: '0.5px'
}}>
{runtimeState?.context?.fanout_pipeline?.length || 0} Agents
</span>
</div>
{/* 活跃分析师列表 */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 14 }}>&#128200;</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111111' }}>活跃分析师</span>
</div>
{(() => {
const activeAnalysts = (runtimeState?.agents || []).filter(
(agent) => agent.status && agent.status !== 'idle' && agent.status !== 'stopped'
);
if (activeAnalysts.length === 0) {
return (
<div style={{
padding: 10,
border: '1px dashed #999999',
background: '#FAFAFA',
fontSize: 11,
color: '#9CA3AF'
}}>
当前无活跃分析师
</div>
);
}
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{activeAnalysts.map((agent) => (
<span
key={agent.agent_id}
style={{
padding: '6px 10px',
fontSize: 10,
fontWeight: 600,
border: '1px solid #059669',
background: '#ECFDF5',
color: '#059669',
textTransform: 'uppercase',
letterSpacing: '0.3px'
}}
>
{agent.agent_id}
</span>
))}
</div>
);
})()}
</div>
{/* 团队配置详情 */}
{runtimeState?.context?.team_config && (
<div style={{ marginTop: 4 }}>
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase', marginBottom: 6 }}>
团队配置
</div>
<pre style={{
margin: 0,
padding: 8,
background: '#FFFFFF',
border: '1px solid #E5E7EB',
fontSize: 10,
lineHeight: 1.5,
color: '#374151',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: '"Courier New", monospace'
}}>
{JSON.stringify(runtimeState.context.team_config, null, 2)}
</pre>
</div>
)}
</div>
</section>
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
{sectionTitle('待审批请求')}
<div style={{
display: 'grid',
gap: 10,
maxHeight: 640,
overflowY: 'auto',
paddingRight: 4
}}>
{approvals.length ? approvals.map((approval) => {
const tone = resolveApprovalTone(approval);
return (
<div
key={approval.approval_id}
style={{
border: `1px solid ${tone.border}`,
background: '#FFFFFF',
padding: 12,
display: 'grid',
gap: 8
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ fontSize: 13, fontWeight: 800, color: '#111111' }}>
{approval.tool_name}
</div>
<div style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.5px',
padding: '4px 6px',
background: tone.badgeBg,
color: tone.text,
border: `1px solid ${tone.border}`,
textTransform: 'uppercase'
}}>
{formatStatusLabel(approval.status)}
</div>
</div>
<div style={{ fontSize: 11, color: '#6B7280', lineHeight: 1.5 }}>
{formatApprovalScopeLabel(approval)}
</div>
{approval.tool_input && (
<pre style={{
margin: 0,
padding: 10,
background: '#FAFAFA',
border: '1px solid #000000',
fontSize: 11,
lineHeight: 1.6,
color: '#111111',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: '"Courier New", monospace'
}}>
{JSON.stringify(approval.tool_input, null, 2)}
</pre>
)}
{approval.findings?.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{approval.findings.map((finding, index) => (
<span
key={`${approval.approval_id}-finding-${index}`}
style={{
padding: '4px 6px',
background: '#FFFFFF',
border: `1px solid ${tone.border}`,
color: tone.text,
fontSize: 10,
fontWeight: 700,
textTransform: 'uppercase'
}}
>
{finding.severity}: {finding.message}
</span>
))}
</div>
)}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<button
onClick={() => handleApprovalAction(approval.approval_id, 'deny')}
disabled={approvalActionId === approval.approval_id}
style={{
padding: '8px 10px',
border: '1px solid #000000',
background: '#FFFFFF',
color: '#000000',
fontSize: 11,
fontWeight: 700,
textTransform: 'uppercase',
cursor: approvalActionId === approval.approval_id ? 'not-allowed' : 'pointer'
}}
>
拒绝
</button>
<button
onClick={() => handleApprovalAction(approval.approval_id, 'approve')}
disabled={approvalActionId === approval.approval_id}
style={{
padding: '8px 10px',
border: '1px solid #000000',
background: '#000000',
color: '#FFFFFF',
fontSize: 11,
fontWeight: 700,
textTransform: 'uppercase',
cursor: approvalActionId === approval.approval_id ? 'not-allowed' : 'pointer'
}}
>
批准
</button>
</div>
</div>
)}) : (
<div style={{
border: '1px dashed #999999',
padding: 16,
fontSize: 12,
color: '#666666',
background: '#FAFAFA'
}}>
当前无待审批请求
</div>
)}
</div>
</section>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'minmax(320px, 1fr) minmax(360px, 1fr)',
gap: 20,
alignItems: 'start'
}}>
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
{sectionTitle('Agent 状态')}
<div style={{
display: 'grid',
gap: 8,
maxHeight: 420,
overflowY: 'auto',
paddingRight: 4
}}>
{runtimeState?.agents?.length ? runtimeState.agents.map((agent) => (
<div
key={agent.agent_id}
style={{
border: '1px solid #000000',
background: '#FAFAFA',
padding: 10,
display: 'grid',
gap: 4
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>{agent.agent_id}</span>
<span style={{ fontSize: 11, color: '#2563EB', fontFamily: '"Courier New", monospace' }}>{formatStatusLabel(agent.status)}</span>
</div>
<div style={{ fontSize: 10, color: '#6B7280', lineHeight: 1.5 }}>
会话: {formatSessionLabel(agent.last_session)}
</div>
<div style={{ fontSize: 10, color: '#6B7280', lineHeight: 1.5 }}>
更新时间: {agent.last_updated}
</div>
<AgentMetricsPanel agent={agent} />
</div>
)) : (
<div style={{ fontSize: 12, color: '#9CA3AF' }}>暂无 agent 状态</div>
)}
</div>
</section>
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
{sectionTitle(
'近期事件',
<select
id="runtime-event-filter"
name="runtime_event_filter"
aria-label="筛选近期事件"
value={eventFilter}
onChange={(event) => setEventFilter(event.target.value)}
style={{
padding: '8px 10px',
border: '1px solid #000000',
background: '#FFFFFF',
color: '#000000',
fontSize: 11,
fontWeight: 700,
textTransform: 'uppercase'
}}
>
{EVENT_FILTER_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
)}
<label htmlFor="runtime-event-filter" style={SR_ONLY_STYLE}>
筛选近期事件
</label>
<div style={{
display: 'grid',
gap: 8,
maxHeight: 420,
overflowY: 'auto',
paddingRight: 4
}}>
{visibleEvents.length ? visibleEvents.map((event, index) => (
<div
key={`${event.timestamp}-${event.event}-${index}`}
style={{
border: '1px solid #000000',
background: '#FAFAFA',
padding: 10,
display: 'grid',
gap: 4
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>{formatEventLabel(event.event)}</span>
<span style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>{formatSessionLabel(event.session)}</span>
</div>
<div style={{ fontSize: 10, color: '#6B7280' }}>{event.timestamp}</div>
{event.details && Object.keys(event.details).length > 0 && (
<pre style={{
margin: 0,
fontSize: 10,
lineHeight: 1.6,
color: '#374151',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: '"Courier New", monospace'
}}>
{JSON.stringify(event.details, null, 2)}
</pre>
)}
</div>
)) : (
<div style={{ fontSize: 12, color: '#9CA3AF' }}>当前筛选条件下暂无运行事件</div>
)}
</div>
</section>
</div>
</div>
</div>
);
}