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
This commit is contained in:
@@ -67,6 +67,7 @@
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.1.0"
|
||||
"vitest": "^4.1.0",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useRuntimeControls } from './hooks/useRuntimeControls';
|
||||
import { useStockDataRequests } from './hooks/useStockDataRequests';
|
||||
import { useWebSocketConnection } from './hooks/useWebSocketConnection';
|
||||
import { fetchRuntimeLogs } from './services/runtimeApi';
|
||||
import { useAgentStore } from './store/agentStore';
|
||||
import { useAgentRunFileState, useAgentStore } from './store/agentStore';
|
||||
import { useMarketStore } from './store/marketStore';
|
||||
import { usePortfolioStore } from './store/portfolioStore';
|
||||
import { useRuntimeStore } from './store/runtimeStore';
|
||||
@@ -82,17 +82,20 @@ export default function LiveTradingApp() {
|
||||
skillDetailLoadingKey,
|
||||
agentSkillsSavingKey,
|
||||
agentSkillsFeedback,
|
||||
selectedWorkspaceFile,
|
||||
workspaceFilesByAgent,
|
||||
workspaceDraftContent,
|
||||
isWorkspaceFileLoading,
|
||||
workspaceFileSavingKey,
|
||||
workspaceFileFeedback,
|
||||
setSelectedWorkspaceFile,
|
||||
setSelectedSkillAgentId,
|
||||
setWorkspaceDraftContent,
|
||||
} = useAgentStore();
|
||||
|
||||
const {
|
||||
selectedRunFile,
|
||||
runFilesByAgent,
|
||||
runDraftContent,
|
||||
isRunFileLoading,
|
||||
runFileSavingKey,
|
||||
runFileFeedback,
|
||||
setRunDraftContent,
|
||||
} = useAgentRunFileState();
|
||||
|
||||
const { feed, processHistoricalFeed, processFeedEvent, addSystemMessage, clearFeed } = useFeedProcessor();
|
||||
const resetRuntimeViewState = useCallback(() => {
|
||||
clearFeed();
|
||||
@@ -177,8 +180,8 @@ export default function LiveTradingApp() {
|
||||
const selectedAgentId = selectedSkillAgentId || AGENTS[0]?.id || null;
|
||||
const selectedAgentProfile = selectedAgentId ? (agentProfilesByAgent[selectedAgentId] || null) : null;
|
||||
const selectedAgentSkills = selectedAgentId ? (agentSkillsByAgent[selectedAgentId] || []) : [];
|
||||
const selectedWorkspaceContent = selectedAgentId && selectedWorkspaceFile
|
||||
? (workspaceFilesByAgent[selectedAgentId]?.[selectedWorkspaceFile] || '')
|
||||
const selectedRunFileContent = selectedAgentId && selectedRunFile
|
||||
? (runFilesByAgent[selectedAgentId]?.[selectedRunFile] || '')
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -188,10 +191,10 @@ export default function LiveTradingApp() {
|
||||
}, [selectedSkillAgentId, setSelectedSkillAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedWorkspaceFile) {
|
||||
if (!selectedRunFile) {
|
||||
setSelectedWorkspaceFile('MEMORY.md');
|
||||
}
|
||||
}, [selectedWorkspaceFile, setSelectedWorkspaceFile]);
|
||||
}, [selectedRunFile, setSelectedWorkspaceFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSocketReady || !selectedAgentId || !clientRef.current) {
|
||||
@@ -207,10 +210,10 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
|
||||
if (
|
||||
selectedWorkspaceFile
|
||||
&& workspaceFilesByAgent[selectedAgentId]?.[selectedWorkspaceFile] === undefined
|
||||
selectedRunFile
|
||||
&& runFilesByAgent[selectedAgentId]?.[selectedRunFile] === undefined
|
||||
) {
|
||||
requestWorkspaceFile(selectedAgentId, selectedWorkspaceFile);
|
||||
requestWorkspaceFile(selectedAgentId, selectedRunFile);
|
||||
}
|
||||
}, [
|
||||
agentProfilesByAgent,
|
||||
@@ -221,8 +224,8 @@ export default function LiveTradingApp() {
|
||||
requestAgentSkills,
|
||||
requestWorkspaceFile,
|
||||
selectedAgentId,
|
||||
selectedWorkspaceFile,
|
||||
workspaceFilesByAgent,
|
||||
selectedRunFile,
|
||||
runFilesByAgent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -361,7 +364,7 @@ export default function LiveTradingApp() {
|
||||
agents: AGENTS,
|
||||
agentProfilesByAgent,
|
||||
agentSkillsByAgent,
|
||||
workspaceFilesByAgent,
|
||||
runFilesByAgent,
|
||||
selectedAgentId,
|
||||
selectedAgentProfile,
|
||||
selectedAgentSkills,
|
||||
@@ -369,16 +372,16 @@ export default function LiveTradingApp() {
|
||||
localSkillDraftsByKey,
|
||||
skillDetailLoadingKey,
|
||||
editableFiles: EDITABLE_AGENT_WORKSPACE_FILES,
|
||||
selectedWorkspaceFile,
|
||||
workspaceFileContent: selectedWorkspaceContent,
|
||||
workspaceDraftContent,
|
||||
selectedRunFile,
|
||||
runFileContent: selectedRunFileContent,
|
||||
runDraftContent,
|
||||
isConnected,
|
||||
isAgentSkillsLoading,
|
||||
agentSkillsSavingKey,
|
||||
agentSkillsFeedback,
|
||||
isWorkspaceFileLoading,
|
||||
workspaceFileSavingKey,
|
||||
workspaceFileFeedback,
|
||||
isRunFileLoading,
|
||||
runFileSavingKey,
|
||||
runFileFeedback,
|
||||
onAgentChange: handleSkillAgentChange,
|
||||
onCreateLocalSkill: handleCreateLocalSkill,
|
||||
onSkillDetailRequest: requestSkillDetail,
|
||||
@@ -388,8 +391,8 @@ export default function LiveTradingApp() {
|
||||
onRemoveSharedSkill: handleRemoveSharedSkill,
|
||||
onSkillToggle: handleAgentSkillToggle,
|
||||
onWorkspaceFileChange: handleWorkspaceFileChange,
|
||||
onWorkspaceDraftChange: setWorkspaceDraftContent,
|
||||
onWorkspaceFileSave: handleWorkspaceFileSave,
|
||||
onRunDraftChange: setRunDraftContent,
|
||||
onRunFileSave: handleWorkspaceFileSave,
|
||||
onUploadExternalSkill: handleUploadExternalSkill,
|
||||
clientRef,
|
||||
};
|
||||
|
||||
@@ -208,7 +208,7 @@ export default function RuntimeSettingsPanel({
|
||||
background: '#FFFFFF',
|
||||
border: '1px dashed #D0D7DE'
|
||||
}}>
|
||||
恢复启动会从所选历史任务复制运行状态、组合、交易记录和 Agent 工作区资产,并以新的任务 ID 继续运行。
|
||||
恢复启动会从所选历史任务复制运行状态、组合、交易记录和 Agent 运行资产,并以新的任务 ID 继续运行。
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -207,6 +207,12 @@ 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 '-';
|
||||
@@ -598,7 +604,7 @@ export default function RuntimeView() {
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280', lineHeight: 1.5 }}>
|
||||
{approval.agent_id} · {approval.workspace_id} · {formatSessionLabel(approval.session_id)}
|
||||
{formatApprovalScopeLabel(approval)}
|
||||
</div>
|
||||
{approval.tool_input && (
|
||||
<pre style={{
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function TraderView({
|
||||
agents,
|
||||
agentProfilesByAgent,
|
||||
agentSkillsByAgent,
|
||||
workspaceFilesByAgent,
|
||||
runFilesByAgent,
|
||||
selectedAgentId,
|
||||
selectedAgentProfile,
|
||||
selectedAgentSkills,
|
||||
@@ -16,16 +16,16 @@ export default function TraderView({
|
||||
localSkillDraftsByKey,
|
||||
skillDetailLoadingKey,
|
||||
editableFiles,
|
||||
selectedWorkspaceFile,
|
||||
workspaceFileContent,
|
||||
workspaceDraftContent,
|
||||
selectedRunFile,
|
||||
runFileContent,
|
||||
runDraftContent,
|
||||
isConnected,
|
||||
isAgentSkillsLoading,
|
||||
agentSkillsSavingKey,
|
||||
agentSkillsFeedback,
|
||||
isWorkspaceFileLoading,
|
||||
workspaceFileSavingKey,
|
||||
workspaceFileFeedback,
|
||||
isRunFileLoading,
|
||||
runFileSavingKey,
|
||||
runFileFeedback,
|
||||
onAgentChange,
|
||||
onCreateLocalSkill,
|
||||
onSkillDetailRequest,
|
||||
@@ -35,8 +35,8 @@ export default function TraderView({
|
||||
onRemoveSharedSkill,
|
||||
onSkillToggle,
|
||||
onWorkspaceFileChange,
|
||||
onWorkspaceDraftChange,
|
||||
onWorkspaceFileSave,
|
||||
onRunDraftChange,
|
||||
onRunFileSave,
|
||||
onUploadExternalSkill
|
||||
}) {
|
||||
const srOnlyStyle = {
|
||||
@@ -133,10 +133,10 @@ export default function TraderView({
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.5px', color: '#111111' }}>
|
||||
交易员档案
|
||||
Agent 运行档案
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
聚焦查看每个 Agent 的模型、工具组、技能编排和工作区记忆,不展示交易表现数据
|
||||
聚焦查看每个 Agent 在当前运行任务中的模型、工具组、技能编排和运行记忆,不展示交易表现数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -549,15 +549,15 @@ export default function TraderView({
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>工作区文件编辑</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>运行文件编辑</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
直接调整该交易员的人设、协作方式和长期记忆文件
|
||||
直接调整该交易员在当前运行任务中的人设、协作方式和长期记忆文件
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{editableFiles.map((filename) => {
|
||||
const isActive = filename === selectedWorkspaceFile;
|
||||
const isActive = filename === selectedRunFile;
|
||||
return (
|
||||
<button
|
||||
key={filename}
|
||||
@@ -581,12 +581,12 @@ export default function TraderView({
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id={`workspace-editor-${selectedAgentId}-${selectedWorkspaceFile || 'file'}`}
|
||||
name={`workspace_editor_${selectedAgentId}_${selectedWorkspaceFile || 'file'}`}
|
||||
aria-label={`编辑 ${selectedWorkspaceFile || '工作区文件'} 内容`}
|
||||
value={workspaceDraftContent}
|
||||
onChange={(e) => onWorkspaceDraftChange(e.target.value)}
|
||||
placeholder={isWorkspaceFileLoading ? '加载中...' : '输入 markdown 内容'}
|
||||
id={`workspace-editor-${selectedAgentId}-${selectedRunFile || 'file'}`}
|
||||
name={`workspace_editor_${selectedAgentId}_${selectedRunFile || 'file'}`}
|
||||
aria-label={`编辑 ${selectedRunFile || '运行文件'} 内容`}
|
||||
value={runDraftContent}
|
||||
onChange={(e) => onRunDraftChange(e.target.value)}
|
||||
placeholder={isRunFileLoading ? '加载中...' : '输入 markdown 内容'}
|
||||
style={{
|
||||
minHeight: 280,
|
||||
resize: 'vertical',
|
||||
@@ -603,33 +603,33 @@ export default function TraderView({
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
|
||||
当前文件: {selectedWorkspaceFile}
|
||||
当前运行文件: {selectedRunFile}
|
||||
</span>
|
||||
<button
|
||||
onClick={onWorkspaceFileSave}
|
||||
disabled={!isConnected || isWorkspaceFileLoading || workspaceFileSavingKey !== null || workspaceDraftContent === workspaceFileContent}
|
||||
onClick={onRunFileSave}
|
||||
disabled={!isConnected || isRunFileLoading || runFileSavingKey !== null || runDraftContent === runFileContent}
|
||||
style={{
|
||||
padding: '9px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? '#0D47A1' : '#94A3B8',
|
||||
background: isConnected && !isRunFileLoading && runFileSavingKey === null && runDraftContent !== runFileContent ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? 'pointer' : 'not-allowed'
|
||||
cursor: isConnected && !isRunFileLoading && runFileSavingKey === null && runDraftContent !== runFileContent ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{workspaceFileSavingKey ? '保存中' : '保存文件'}
|
||||
{runFileSavingKey ? '保存中' : '保存文件'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{workspaceFileFeedback && (
|
||||
{runFileFeedback && (
|
||||
<span style={{
|
||||
color: workspaceFileFeedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
color: runFileFeedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{workspaceFileFeedback.text}
|
||||
{runFileFeedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -40,13 +40,13 @@ export function useAgentDataRequests(clientRef) {
|
||||
setIsWorkspaceFileLoading
|
||||
} = useAgentStore();
|
||||
|
||||
const resolveWorkspaceId = useCallback(async () => {
|
||||
const resolveRunId = useCallback(async () => {
|
||||
const runtime = await fetchCurrentRuntime();
|
||||
const workspaceId = runtime?.run_id;
|
||||
if (!workspaceId) {
|
||||
const runId = runtime?.run_id;
|
||||
if (!runId) {
|
||||
throw new Error('未检测到正在运行的任务');
|
||||
}
|
||||
return workspaceId;
|
||||
return runId;
|
||||
}, []);
|
||||
|
||||
const requestAgentSkills = useCallback((agentId) => {
|
||||
@@ -54,8 +54,8 @@ export function useAgentDataRequests(clientRef) {
|
||||
if (!normalized) return false;
|
||||
setIsAgentSkillsLoading(true);
|
||||
setAgentSkillsFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => fetchAgentSkills(workspaceId, normalized))
|
||||
void resolveRunId()
|
||||
.then((runId) => fetchAgentSkills(runId, normalized))
|
||||
.then((payload) => {
|
||||
setAgentSkillsByAgent((prev) => ({ ...prev, [normalized]: Array.isArray(payload?.skills) ? payload.skills : [] }));
|
||||
setIsAgentSkillsLoading(false);
|
||||
@@ -72,13 +72,13 @@ export function useAgentDataRequests(clientRef) {
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, [clientRef, resolveWorkspaceId, setAgentSkillsByAgent, setIsAgentSkillsLoading, setAgentSkillsFeedback]);
|
||||
}, [clientRef, resolveRunId, setAgentSkillsByAgent, setIsAgentSkillsLoading, setAgentSkillsFeedback]);
|
||||
|
||||
const requestAgentProfile = useCallback((agentId) => {
|
||||
const normalized = typeof agentId === 'string' ? agentId.trim() : '';
|
||||
if (!normalized) return false;
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => fetchAgentProfile(workspaceId, normalized))
|
||||
void resolveRunId()
|
||||
.then((runId) => fetchAgentProfile(runId, normalized))
|
||||
.then((payload) => {
|
||||
setAgentProfilesByAgent((prev) => ({
|
||||
...prev,
|
||||
@@ -92,15 +92,15 @@ export function useAgentDataRequests(clientRef) {
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, [clientRef, resolveWorkspaceId, setAgentProfilesByAgent]);
|
||||
}, [clientRef, resolveRunId, setAgentProfilesByAgent]);
|
||||
|
||||
const requestSkillDetail = useCallback((skillName) => {
|
||||
const normalized = typeof skillName === 'string' ? skillName.trim() : '';
|
||||
if (!normalized) return false;
|
||||
const detailKey = `${selectedSkillAgentId}:${normalized}`;
|
||||
setSkillDetailLoadingKey(detailKey);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => fetchAgentSkillDetail(workspaceId, selectedSkillAgentId, normalized))
|
||||
void resolveRunId()
|
||||
.then((runId) => fetchAgentSkillDetail(runId, selectedSkillAgentId, normalized))
|
||||
.then((payload) => {
|
||||
setSkillDetailsByName((prev) => ({ ...prev, [detailKey]: payload?.skill || null }));
|
||||
useAgentStore.getState().setLocalSkillDraftsByKey((prev) => ({
|
||||
@@ -121,7 +121,7 @@ export function useAgentDataRequests(clientRef) {
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, [clientRef, resolveWorkspaceId, selectedSkillAgentId, setSkillDetailLoadingKey, setSkillDetailsByName]);
|
||||
}, [clientRef, resolveRunId, selectedSkillAgentId, setSkillDetailLoadingKey, setSkillDetailsByName]);
|
||||
|
||||
const handleCreateLocalSkill = useCallback((skillName) => {
|
||||
const normalized = typeof skillName === 'string' ? skillName.trim() : '';
|
||||
@@ -131,8 +131,8 @@ export function useAgentDataRequests(clientRef) {
|
||||
}
|
||||
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${normalized}:create`);
|
||||
setAgentSkillsFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => createAgentLocalSkill(workspaceId, selectedSkillAgentId, normalized))
|
||||
void resolveRunId()
|
||||
.then((runId) => createAgentLocalSkill(runId, selectedSkillAgentId, normalized))
|
||||
.then(() => {
|
||||
setAgentSkillsSavingKey(null);
|
||||
setAgentSkillsFeedback({ type: 'success', text: `已创建本地技能 ${normalized}` });
|
||||
@@ -152,7 +152,7 @@ export function useAgentDataRequests(clientRef) {
|
||||
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
});
|
||||
}, [clientRef, requestAgentSkills, requestSkillDetail, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
}, [clientRef, requestAgentSkills, requestSkillDetail, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
|
||||
const handleLocalSkillDraftChange = useCallback((skillName, content) => {
|
||||
const detailKey = `${selectedSkillAgentId}:${skillName}`;
|
||||
@@ -165,8 +165,8 @@ export function useAgentDataRequests(clientRef) {
|
||||
if (typeof content !== 'string') return;
|
||||
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:content`);
|
||||
setAgentSkillsFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => updateAgentLocalSkill(workspaceId, selectedSkillAgentId, skillName, content))
|
||||
void resolveRunId()
|
||||
.then((runId) => updateAgentLocalSkill(runId, selectedSkillAgentId, skillName, content))
|
||||
.then(() => {
|
||||
setAgentSkillsSavingKey(null);
|
||||
setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 的本地技能 ${skillName} 已保存` });
|
||||
@@ -185,13 +185,13 @@ export function useAgentDataRequests(clientRef) {
|
||||
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
});
|
||||
}, [clientRef, localSkillDraftsByKey, requestSkillDetail, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
}, [clientRef, localSkillDraftsByKey, requestSkillDetail, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
|
||||
const handleLocalSkillDelete = useCallback((skillName) => {
|
||||
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:delete`);
|
||||
setAgentSkillsFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => deleteAgentLocalSkill(workspaceId, selectedSkillAgentId, skillName))
|
||||
void resolveRunId()
|
||||
.then((runId) => deleteAgentLocalSkill(runId, selectedSkillAgentId, skillName))
|
||||
.then(() => {
|
||||
setAgentSkillsSavingKey(null);
|
||||
setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 的本地技能 ${skillName} 已删除` });
|
||||
@@ -210,13 +210,13 @@ export function useAgentDataRequests(clientRef) {
|
||||
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
});
|
||||
}, [clientRef, requestAgentSkills, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
}, [clientRef, requestAgentSkills, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
|
||||
const handleRemoveSharedSkill = useCallback((skillName) => {
|
||||
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:remove`);
|
||||
setAgentSkillsFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => disableAgentSkill(workspaceId, selectedSkillAgentId, skillName))
|
||||
void resolveRunId()
|
||||
.then((runId) => disableAgentSkill(runId, selectedSkillAgentId, skillName))
|
||||
.then(() => {
|
||||
setAgentSkillsSavingKey(null);
|
||||
setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 已移除共享技能 ${skillName}` });
|
||||
@@ -235,16 +235,16 @@ export function useAgentDataRequests(clientRef) {
|
||||
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
});
|
||||
}, [clientRef, requestAgentSkills, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
}, [clientRef, requestAgentSkills, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
|
||||
const handleAgentSkillToggle = useCallback((skillName, enabled) => {
|
||||
const agentId = selectedSkillAgentId;
|
||||
setAgentSkillsSavingKey(`${agentId}:${skillName}`);
|
||||
setAgentSkillsFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => enabled
|
||||
? enableAgentSkill(workspaceId, agentId, skillName)
|
||||
: disableAgentSkill(workspaceId, agentId, skillName))
|
||||
void resolveRunId()
|
||||
.then((runId) => enabled
|
||||
? enableAgentSkill(runId, agentId, skillName)
|
||||
: disableAgentSkill(runId, agentId, skillName))
|
||||
.then(() => {
|
||||
setAgentSkillsSavingKey(null);
|
||||
setAgentSkillsFeedback({ type: 'success', text: `${agentId} ${enabled ? '已启用' : '已禁用'} ${skillName}` });
|
||||
@@ -263,7 +263,7 @@ export function useAgentDataRequests(clientRef) {
|
||||
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
});
|
||||
}, [clientRef, requestAgentSkills, resolveWorkspaceId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
}, [clientRef, requestAgentSkills, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
|
||||
|
||||
const handleSkillAgentChange = useCallback((agentId) => {
|
||||
setSelectedSkillAgentId(agentId);
|
||||
@@ -278,8 +278,8 @@ export function useAgentDataRequests(clientRef) {
|
||||
if (!normalizedAgentId || !normalizedFilename) return false;
|
||||
setIsWorkspaceFileLoading(true);
|
||||
setWorkspaceFileFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => fetchAgentWorkspaceFile(workspaceId, normalizedAgentId, normalizedFilename))
|
||||
void resolveRunId()
|
||||
.then((runId) => fetchAgentWorkspaceFile(runId, normalizedAgentId, normalizedFilename))
|
||||
.then((payload) => {
|
||||
setWorkspaceFilesByAgent((prev) => ({
|
||||
...prev,
|
||||
@@ -303,7 +303,7 @@ export function useAgentDataRequests(clientRef) {
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}, [clientRef, resolveWorkspaceId, setIsWorkspaceFileLoading, setWorkspaceDraftContent, setWorkspaceFileFeedback, setWorkspaceFilesByAgent]);
|
||||
}, [clientRef, resolveRunId, setIsWorkspaceFileLoading, setWorkspaceDraftContent, setWorkspaceFileFeedback, setWorkspaceFilesByAgent]);
|
||||
|
||||
const handleWorkspaceFileChange = useCallback((filename) => {
|
||||
useAgentStore.getState().setSelectedWorkspaceFile(filename);
|
||||
@@ -314,8 +314,8 @@ export function useAgentDataRequests(clientRef) {
|
||||
const key = `${selectedSkillAgentId}:${selectedWorkspaceFile}`;
|
||||
setWorkspaceFileSavingKey(key);
|
||||
setWorkspaceFileFeedback(null);
|
||||
void resolveWorkspaceId()
|
||||
.then((workspaceId) => updateAgentWorkspaceFile(workspaceId, selectedSkillAgentId, selectedWorkspaceFile, workspaceDraftContent))
|
||||
void resolveRunId()
|
||||
.then((runId) => updateAgentWorkspaceFile(runId, selectedSkillAgentId, selectedWorkspaceFile, workspaceDraftContent))
|
||||
.then((payload) => {
|
||||
setWorkspaceFileSavingKey(null);
|
||||
setWorkspaceFileFeedback({ type: 'success', text: `${selectedSkillAgentId} 的 ${selectedWorkspaceFile} 已保存` });
|
||||
@@ -345,7 +345,7 @@ export function useAgentDataRequests(clientRef) {
|
||||
setWorkspaceFileFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
});
|
||||
}, [clientRef, resolveWorkspaceId, selectedSkillAgentId, selectedWorkspaceFile, setWorkspaceFileFeedback, setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, workspaceDraftContent]);
|
||||
}, [clientRef, resolveRunId, selectedSkillAgentId, selectedWorkspaceFile, setWorkspaceFileFeedback, setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, workspaceDraftContent]);
|
||||
|
||||
const handleUploadExternalSkill = useCallback(async (file) => {
|
||||
if (!(file instanceof File)) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* useWebsocketSessionSync - DEPRECATED
|
||||
*
|
||||
* This hook is deprecated. WebSocket connection and event handling is now managed
|
||||
* by useWebSocketConnection.js. This file is kept for backwards compatibility
|
||||
* but will be removed in a future version.
|
||||
*
|
||||
* All functionality has been consolidated into:
|
||||
* - useWebSocketConnection.js: WebSocket lifecycle and event handlers
|
||||
* - useStockDataRequests.js: Stock data request callbacks
|
||||
* - useAgentDataRequests.js: Agent operation callbacks
|
||||
*/
|
||||
|
||||
import { useWebSocketConnection } from './useWebSocketConnection';
|
||||
|
||||
/**
|
||||
* @deprecated Use useWebSocketConnection directly instead.
|
||||
* This hook is a thin wrapper that delegates to useWebSocketConnection
|
||||
* for backwards compatibility.
|
||||
*/
|
||||
export function useWebsocketSessionSync(props) {
|
||||
// Delegate to useWebSocketConnection
|
||||
const { clientRef } = useWebSocketConnection();
|
||||
|
||||
// Return clientRef so existing code can still access it
|
||||
return { clientRef };
|
||||
}
|
||||
|
||||
export default useWebsocketSessionSync;
|
||||
@@ -129,56 +129,64 @@ export function fetchRuntimeLogs() {
|
||||
return safeFetch(RUNTIME_API_BASE, '/logs');
|
||||
}
|
||||
|
||||
export function fetchAgentProfile(workspaceId, agentId) {
|
||||
return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/profile`);
|
||||
function buildRunScopedAgentPath(runId, agentId, suffix = '') {
|
||||
return `/workspaces/${encodeURIComponent(runId)}/agents/${encodeURIComponent(agentId)}${suffix}`;
|
||||
}
|
||||
|
||||
export function fetchAgentSkills(workspaceId, agentId) {
|
||||
return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills`);
|
||||
/**
|
||||
* Runtime-read agent routes still use the `/workspaces/...` prefix on the
|
||||
* backend, but the leading identifier on this surface is the active `run_id`.
|
||||
*/
|
||||
export function fetchAgentProfile(runId, agentId) {
|
||||
return safeFetch(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, '/profile'));
|
||||
}
|
||||
|
||||
export function fetchAgentSkillDetail(workspaceId, agentId, skillName) {
|
||||
return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skillName)}`);
|
||||
export function fetchAgentSkills(runId, agentId) {
|
||||
return safeFetch(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, '/skills'));
|
||||
}
|
||||
|
||||
export function fetchAgentWorkspaceFile(workspaceId, agentId, filename) {
|
||||
return safeFetch(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/files/${encodeURIComponent(filename)}`);
|
||||
export function fetchAgentSkillDetail(runId, agentId, skillName) {
|
||||
return safeFetch(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, `/skills/${encodeURIComponent(skillName)}`));
|
||||
}
|
||||
|
||||
export function createAgentLocalSkill(workspaceId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/local`, {
|
||||
export function fetchAgentWorkspaceFile(runId, agentId, filename) {
|
||||
return safeFetch(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, `/files/${encodeURIComponent(filename)}`));
|
||||
}
|
||||
|
||||
export function createAgentLocalSkill(runId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, '/skills/local'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ skill_name: skillName })
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAgentLocalSkill(workspaceId, agentId, skillName, content) {
|
||||
return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/local/${encodeURIComponent(skillName)}`, {
|
||||
export function updateAgentLocalSkill(runId, agentId, skillName, content) {
|
||||
return safeRequest(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, `/skills/local/${encodeURIComponent(skillName)}`), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteAgentLocalSkill(workspaceId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/local/${encodeURIComponent(skillName)}`, {
|
||||
export function deleteAgentLocalSkill(runId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, `/skills/local/${encodeURIComponent(skillName)}`), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
export function enableAgentSkill(workspaceId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skillName)}/enable`, {
|
||||
export function enableAgentSkill(runId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, `/skills/${encodeURIComponent(skillName)}/enable`), {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function disableAgentSkill(workspaceId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, `/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skillName)}/disable`, {
|
||||
export function disableAgentSkill(runId, agentId, skillName) {
|
||||
return safeRequest(CONTROL_API_BASE, buildRunScopedAgentPath(runId, agentId, `/skills/${encodeURIComponent(skillName)}/disable`), {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function updateAgentWorkspaceFile(workspaceId, agentId, filename, content) {
|
||||
return fetch(`${CONTROL_API_BASE}/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/files/${encodeURIComponent(filename)}`, {
|
||||
export function updateAgentWorkspaceFile(runId, agentId, filename, content) {
|
||||
return fetch(`${CONTROL_API_BASE}${buildRunScopedAgentPath(runId, agentId, `/files/${encodeURIComponent(filename)}`)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
@@ -206,8 +214,8 @@ export async function uploadAgentSkillZip({
|
||||
throw new Error('valid zip file is required');
|
||||
}
|
||||
const runtime = runId ? { run_id: runId } : await fetchCurrentRuntime();
|
||||
const workspaceId = runtime?.run_id;
|
||||
if (!workspaceId) {
|
||||
const resolvedRunId = runtime?.run_id;
|
||||
if (!resolvedRunId) {
|
||||
throw new Error('未检测到正在运行的任务');
|
||||
}
|
||||
|
||||
@@ -220,7 +228,7 @@ export async function uploadAgentSkillZip({
|
||||
|
||||
return safeRequest(
|
||||
CONTROL_API_BASE,
|
||||
`/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/upload`,
|
||||
buildRunScopedAgentPath(resolvedRunId, agentId, '/skills/upload'),
|
||||
{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
|
||||
45
frontend/src/services/runtimeApi.test.js
Normal file
45
frontend/src/services/runtimeApi.test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
fetchAgentProfile,
|
||||
updateAgentWorkspaceFile
|
||||
} from './runtimeApi';
|
||||
|
||||
describe('runtimeApi run-scoped agent routes', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('uses run_id in runtime-read agent profile requests', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ profile: {}, scope_type: 'runtime_run' })
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await fetchAgentProfile('20260330_123000', 'portfolio_manager');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/workspaces/20260330_123000/agents/portfolio_manager/profile')
|
||||
);
|
||||
});
|
||||
|
||||
it('uses run_id in runtime agent file update requests', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ content: '# demo' }),
|
||||
text: async () => ''
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await updateAgentWorkspaceFile('20260330_123000', 'risk_manager', 'MEMORY.md', '# demo');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/workspaces/20260330_123000/agents/risk_manager/files/MEMORY.md'),
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: '# demo'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,8 @@ const resolveValue = (updater, currentValue) => (
|
||||
);
|
||||
|
||||
/**
|
||||
* Agent Store - Agent skills, profiles, workspaces
|
||||
* Agent Store - Agent skills, profiles, design-time workspace terminology, and
|
||||
* run-scoped file editing state.
|
||||
*/
|
||||
export const useAgentStore = create((set) => ({
|
||||
// Selected agent for skill/workspace editing
|
||||
@@ -60,3 +61,18 @@ export const useAgentStore = create((set) => ({
|
||||
workspaceFileFeedback: null,
|
||||
setWorkspaceFileFeedback: (workspaceFileFeedback) => set((state) => ({ workspaceFileFeedback: resolveValue(workspaceFileFeedback, state.workspaceFileFeedback) })),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Run-scoped file editing state currently reuses legacy `workspace*` field
|
||||
* names inside the store. Prefer this selector for new runtime UI code.
|
||||
*/
|
||||
export const useAgentRunFileState = () => useAgentStore((state) => ({
|
||||
selectedRunFile: state.selectedWorkspaceFile,
|
||||
runFilesByAgent: state.workspaceFilesByAgent,
|
||||
runDraftContent: state.workspaceDraftContent,
|
||||
isRunFileLoading: state.isWorkspaceFileLoading,
|
||||
runFileSavingKey: state.workspaceFileSavingKey,
|
||||
runFileFeedback: state.workspaceFileFeedback,
|
||||
setSelectedRunFile: state.setSelectedWorkspaceFile,
|
||||
setRunDraftContent: state.setWorkspaceDraftContent,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user