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
813 lines
28 KiB
JavaScript
813 lines
28 KiB
JavaScript
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 }}>📣</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 }}>👥</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 }}>📈</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>
|
||
);
|
||
}
|