feat: Add agent workspace system and runtime management

- Add agent core modules (agent_core, factory, registry, skill_loader)
- Add runtime system for agent execution management
- Add REST API for agents, workspaces, and runtime control
- Add process supervisor for agent lifecycle management
- Add workspace template system with agent profiles
- Add frontend RuntimeView and runtime API integration
- Add per-agent skill workspaces for smoke_fullstack run
- Refactor skill system with active/installed separation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 16:43:29 +08:00
parent 2daf5717ba
commit 59b44545d0
121 changed files with 8384 additions and 358 deletions

View File

@@ -47,7 +47,7 @@ function getRankMedal(rank) {
* Supports click and hover (1.5s) to show agent performance cards
* Supports replay mode - completely independent from live mode
*/
export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJumpToMessage }) {
export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJumpToMessage, onOpenLaunchConfig }) {
const canvasRef = useRef(null);
const containerRef = useRef(null);
@@ -719,13 +719,24 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
/>
)}
{/* Replay Button */}
{showReplayButton && (
{/* Room Controls */}
{(showReplayButton || onOpenLaunchConfig) && (
<div className="replay-button-container">
{onOpenLaunchConfig && (
<button
className="replay-button"
onClick={onOpenLaunchConfig}
title="打开启动配置"
style={{ background: '#FFFFFF', color: '#000000' }}
>
<span>启动</span>
</button>
)}
<button
className="replay-button"
onClick={handleReplayClick}
title="Replay feed history"
disabled={!showReplayButton}
>
<span className="replay-icon">&#9654;&#9654;</span>
<span>回放</span>
@@ -767,4 +778,3 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
</div>
);
}

View File

@@ -1,247 +1,471 @@
import React from 'react';
import { createPortal } from 'react-dom';
export default function RuntimeSettingsPanel({
showTrigger = true,
isOpen,
isConnected,
isSaving,
feedback,
runtimeConfig,
scheduleMode,
intervalMinutes,
triggerTime,
maxCommCycles,
initialCash,
marginRequirement,
enableMemory,
watchlistSymbols,
watchlistInputValue,
watchlistSuggestions,
onToggle,
onClose,
onScheduleModeChange,
onIntervalMinutesChange,
onTriggerTimeChange,
onMaxCommCyclesChange,
onInitialCashChange,
onMarginRequirementChange,
onEnableMemoryChange,
onWatchlistInputChange,
onWatchlistInputKeyDown,
onWatchlistAdd,
onWatchlistRemove,
onWatchlistRestoreCurrent,
onWatchlistRestoreDefault,
onWatchlistSuggestionClick,
onSave,
onRestoreDefaults
}) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, position: 'relative' }}>
<button
onClick={onToggle}
style={{
padding: '6px 10px',
borderRadius: 4,
border: '1px solid #333333',
background: isOpen ? '#1E1E1E' : '#111111',
color: '#FFFFFF',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.6px',
cursor: 'pointer',
whiteSpace: 'nowrap'
}}
>
运行设置
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
{showTrigger && (
<button
onClick={onToggle}
style={{
padding: '6px 10px',
borderRadius: 4,
border: '1px solid #333333',
background: isOpen ? '#1E1E1E' : '#111111',
color: '#FFFFFF',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.6px',
cursor: 'pointer',
whiteSpace: 'nowrap'
}}
>
启动配置
</button>
)}
{isOpen && (
<div style={{
position: 'absolute',
top: 'calc(100% + 10px)',
right: 0,
width: 320,
maxWidth: 'min(320px, 92vw)',
padding: '14px',
borderRadius: 8,
border: '1px solid #D9D9D9',
background: '#FFFFFF',
boxShadow: '0 12px 36px rgba(0, 0, 0, 0.14)',
zIndex: 40,
display: 'grid',
gap: 12
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
<div>
<div style={{ fontSize: '12px', fontWeight: 700, color: '#111111', letterSpacing: '0.3px' }}>
运行设置
</div>
<div style={{ fontSize: '11px', color: '#666666', marginTop: 2 }}>
保存后立即热更新当前运行中的调度参数
</div>
</div>
{isOpen && createPortal((
<div
onClick={onClose}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(15, 23, 42, 0.28)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
zIndex: 9998
}}
>
<div
onClick={(event) => event.stopPropagation()}
style={{
width: 'min(760px, 92vw)',
maxHeight: '80vh',
overflowY: 'auto',
borderRadius: 16,
border: '1px solid #D9E0E7',
background: '#FFFFFF',
boxShadow: '0 24px 60px rgba(15, 23, 42, 0.18)',
padding: 18,
paddingTop: 22,
display: 'grid',
gap: 16,
position: 'relative',
zIndex: 9999
}}
>
<button
onClick={onClose}
style={{
border: 'none',
background: 'transparent',
color: '#666666',
position: 'absolute',
top: 16,
right: 16,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
borderRadius: 999,
width: 40,
height: 40,
fontSize: 16,
lineHeight: 1,
color: '#111111',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontSize: '14px',
lineHeight: 1
boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)'
}}
aria-label="关闭启动配置"
>
×
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>调度模式</span>
<select
value={scheduleMode}
onChange={(e) => onScheduleModeChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px'
}}
>
<option value="daily">daily</option>
<option value="intraday">intraday</option>
</select>
</label>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>间隔(分钟)</span>
<input
type="number"
min="1"
value={intervalMinutes}
onChange={(e) => onIntervalMinutesChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
</div>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>Daily 时间 (NYSE)</span>
<input
type="time"
value={triggerTime}
onChange={(e) => onTriggerTimeChange(e.target.value)}
disabled={scheduleMode !== 'daily'}
style={{
padding: '9px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: scheduleMode === 'daily' ? '#FFFFFF' : '#F3F4F6',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>讨论轮数上限</span>
<input
type="number"
min="1"
value={maxCommCycles}
onChange={(e) => onMaxCommCyclesChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<button
onClick={onRestoreDefaults}
style={{
padding: '9px 12px',
borderRadius: 6,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
恢复默认
</button>
<button
onClick={onSave}
disabled={!isConnected || isSaving}
style={{
padding: '9px 14px',
borderRadius: 6,
border: '1px solid #1565C0',
background: isConnected && !isSaving ? '#0D47A1' : '#94A3B8',
color: '#FFFFFF',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.4px',
cursor: isConnected && !isSaving ? 'pointer' : 'not-allowed'
}}
>
{isSaving ? '保存中' : '保存运行配置'}
</button>
</div>
{feedback && (
<span style={{
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
fontSize: '11px',
fontFamily: '"Courier New", monospace'
}}>
{feedback.text}
</span>
)}
{runtimeConfig && (
<div style={{
borderTop: '1px solid #E5E7EB',
paddingTop: 12,
display: 'grid',
gap: 8
}}>
<div>
<div style={{ fontSize: '12px', fontWeight: 700, color: '#111111', letterSpacing: '0.3px' }}>
当前生效配置
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', paddingRight: 56 }}>
<div style={{ display: 'grid', gap: 4 }}>
<div style={{ fontSize: 14, fontWeight: 800, color: '#111111' }}>启动配置</div>
<div style={{ fontSize: 11, color: '#6B7280' }}>
配置本次任务的启动参数与调度方式
</div>
<div style={{ fontSize: '11px', color: '#666666', marginTop: 2 }}>
这里显示当前 run 已加载并生效的参数
</div>
</div>
<div style={{
border: '1px solid #E5E7EB',
background: '#F8FAFC',
borderRadius: 6,
padding: '10px 12px',
display: 'grid',
gap: 6,
fontSize: '11px',
fontFamily: '"Courier New", monospace',
color: '#111111'
}}>
<div>tickers: {(runtimeConfig.tickers || []).join(', ') || '-'}</div>
<div>schedule_mode: {runtimeConfig.schedule_mode || '-'}</div>
<div>interval_minutes: {runtimeConfig.interval_minutes ?? '-'}</div>
<div>trigger_time: {runtimeConfig.trigger_time || '-'}</div>
<div>max_comm_cycles: {runtimeConfig.max_comm_cycles ?? '-'}</div>
<div>initial_cash: {runtimeConfig.initial_cash ?? '-'}</div>
<div>margin_requirement: {runtimeConfig.margin_requirement ?? '-'}</div>
<div>enable_memory: {String(runtimeConfig.enable_memory ?? false)}</div>
</div>
</div>
)}
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 12
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>自选股</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 8,
minHeight: 40,
padding: '2px 0'
}}>
{watchlistSymbols.map((symbol) => (
<button
key={symbol}
onClick={() => onWatchlistRemove(symbol)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
padding: '6px 10px',
borderRadius: 999,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
<span>{symbol}</span>
<span style={{ color: '#777777' }}>×</span>
</button>
))}
{watchlistSymbols.length === 0 && (
<div style={{ fontSize: '11px', color: '#888888', padding: '8px 2px' }}>
还没有股票输入代码后回车添加
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<input
value={watchlistInputValue}
onChange={(e) => onWatchlistInputChange(e.target.value)}
onKeyDown={onWatchlistInputKeyDown}
placeholder="输入股票代码,回车添加"
style={{
flex: 1,
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
<button
onClick={onWatchlistAdd}
style={{
padding: '9px 12px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
添加
</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{watchlistSuggestions.map((symbol) => {
const active = watchlistSymbols.includes(symbol);
return (
<button
key={symbol}
onClick={() => onWatchlistSuggestionClick(symbol)}
disabled={active}
style={{
padding: '5px 8px',
borderRadius: 999,
border: '1px solid',
borderColor: active ? '#B6E3C5' : '#D0D7DE',
background: active ? '#ECFDF3' : '#FFFFFF',
color: active ? '#157347' : '#4A5568',
fontSize: '10px',
fontWeight: 700,
cursor: active ? 'default' : 'pointer'
}}
>
{symbol}
</button>
);
})}
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
onClick={onWatchlistRestoreCurrent}
style={{
padding: '8px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
恢复当前
</button>
<button
onClick={onWatchlistRestoreDefault}
style={{
padding: '8px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
恢复默认
</button>
</div>
</div>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 12
}}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>调度参数</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>调度模式</span>
<select
value={scheduleMode}
onChange={(e) => onScheduleModeChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px'
}}
>
<option value="daily">每日定时</option>
<option value="intraday">盘中轮询</option>
</select>
</label>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>间隔(分钟)</span>
<input
type="number"
min="1"
value={intervalMinutes}
onChange={(e) => onIntervalMinutesChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
</div>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>每日定时时间 (NYSE)</span>
<input
type="time"
value={triggerTime}
onChange={(e) => onTriggerTimeChange(e.target.value)}
disabled={scheduleMode !== 'daily'}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: scheduleMode === 'daily' ? '#FFFFFF' : '#F3F4F6',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>讨论轮数上限</span>
<input
type="number"
min="1"
value={maxCommCycles}
onChange={(e) => onMaxCommCyclesChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>初始资金</span>
<input
type="number"
min="1"
step="1000"
value={initialCash}
onChange={(e) => onInitialCashChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
<label style={{ display: 'grid', gap: 4 }}>
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>保证金要求</span>
<input
type="number"
min="0"
step="0.01"
value={marginRequirement}
onChange={(e) => onMarginRequirementChange(e.target.value)}
style={{
padding: '9px 10px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '12px',
fontFamily: '"Courier New", monospace'
}}
/>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 2 }}>
<input
type="checkbox"
checked={enableMemory}
onChange={(e) => onEnableMemoryChange(e.target.checked)}
style={{
width: 16,
height: 16,
accentColor: '#0D47A1',
cursor: 'pointer'
}}
/>
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用长期记忆</span>
</label>
</div>
<div style={{
border: '1px solid #E5EAF1',
borderRadius: 12,
background: '#FCFDFE',
padding: 14,
display: 'grid',
gap: 10
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>操作</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<button
onClick={onRestoreDefaults}
style={{
padding: '9px 12px',
borderRadius: 8,
border: '1px solid #D0D7DE',
background: '#FFFFFF',
color: '#111111',
fontSize: '11px',
fontWeight: 700,
cursor: 'pointer'
}}
>
恢复默认
</button>
<button
onClick={onSave}
disabled={!isConnected || isSaving}
style={{
padding: '9px 14px',
borderRadius: 8,
border: '1px solid #1565C0',
background: isConnected && !isSaving ? '#0D47A1' : '#94A3B8',
color: '#FFFFFF',
fontSize: '11px',
fontWeight: 700,
letterSpacing: '0.4px',
cursor: isConnected && !isSaving ? 'pointer' : 'not-allowed'
}}
>
{isSaving ? '启动中' : '启动任务'}
</button>
</div>
</div>
{feedback && (
<span style={{
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
fontSize: '11px',
fontFamily: '"Courier New", monospace'
}}>
{feedback.text}
</span>
)}
</div>
</div>
</div>
)}
), document.body)}
</div>
);
}

View File

@@ -0,0 +1,556 @@
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: '审批事件' }
];
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' };
}
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 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={{
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 }}>
{approval.agent_id} · {approval.workspace_id} · {formatSessionLabel(approval.session_id)}
</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>
</div>
)) : (
<div style={{ fontSize: 12, color: '#9CA3AF' }}>暂无 agent 状态</div>
)}
</div>
</section>
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
{sectionTitle(
'近期事件',
<select
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>
)}
<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>
);
}