Files
evotraders/frontend/src/hooks/useAgentDataRequests.js
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

389 lines
17 KiB
JavaScript

import { useCallback } from 'react';
import {
createAgentLocalSkill,
deleteAgentLocalSkill,
disableAgentSkill,
enableAgentSkill,
fetchAgentProfile,
fetchAgentSkillDetail,
fetchAgentSkills,
fetchAgentWorkspaceFile,
fetchCurrentRuntime,
updateAgentLocalSkill,
updateAgentWorkspaceFile,
uploadAgentSkillZip
} from '../services/runtimeApi';
import { useAgentStore } from '../store/agentStore';
/**
* Custom hook for agent operation callbacks.
* Takes clientRef, uses agentStore.
*/
export function useAgentDataRequests(clientRef) {
const {
selectedSkillAgentId,
setSelectedSkillAgentId,
setAgentProfilesByAgent,
setIsAgentSkillsLoading,
setAgentSkillsFeedback,
setAgentSkillsSavingKey,
setSkillDetailLoadingKey,
setAgentSkillsByAgent,
setSkillDetailsByName,
localSkillDraftsByKey,
selectedWorkspaceFile,
setWorkspaceFilesByAgent,
setWorkspaceDraftContent,
workspaceDraftContent,
setWorkspaceFileFeedback,
setWorkspaceFileSavingKey,
setIsWorkspaceFileLoading
} = useAgentStore();
const resolveRunId = useCallback(async () => {
const runtime = await fetchCurrentRuntime();
const runId = runtime?.run_id;
if (!runId) {
throw new Error('未检测到正在运行的任务');
}
return runId;
}, []);
const requestAgentSkills = useCallback((agentId) => {
const normalized = typeof agentId === 'string' ? agentId.trim() : '';
if (!normalized) return false;
setIsAgentSkillsLoading(true);
setAgentSkillsFeedback(null);
void resolveRunId()
.then((runId) => fetchAgentSkills(runId, normalized))
.then((payload) => {
setAgentSkillsByAgent((prev) => ({ ...prev, [normalized]: Array.isArray(payload?.skills) ? payload.skills : [] }));
setIsAgentSkillsLoading(false);
})
.catch(() => {
if (!clientRef.current) {
setIsAgentSkillsLoading(false);
return;
}
console.debug('REST agent skills request failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'get_agent_skills', agent_id: normalized });
if (!success) {
setIsAgentSkillsLoading(false);
}
});
return true;
}, [clientRef, resolveRunId, setAgentSkillsByAgent, setIsAgentSkillsLoading, setAgentSkillsFeedback]);
const requestAgentProfile = useCallback((agentId) => {
const normalized = typeof agentId === 'string' ? agentId.trim() : '';
if (!normalized) return false;
void resolveRunId()
.then((runId) => fetchAgentProfile(runId, normalized))
.then((payload) => {
setAgentProfilesByAgent((prev) => ({
...prev,
[normalized]: payload?.profile && typeof payload.profile === 'object' ? payload.profile : {}
}));
})
.catch(() => {
if (clientRef.current) {
console.debug('REST agent profile request failed, falling back to websocket compatibility path');
clientRef.current.send({ type: 'get_agent_profile', agent_id: normalized });
}
});
return true;
}, [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 resolveRunId()
.then((runId) => fetchAgentSkillDetail(runId, selectedSkillAgentId, normalized))
.then((payload) => {
setSkillDetailsByName((prev) => ({ ...prev, [detailKey]: payload?.skill || null }));
useAgentStore.getState().setLocalSkillDraftsByKey((prev) => ({
...prev,
[detailKey]: typeof payload?.skill?.content === 'string' ? payload.skill.content : ''
}));
setSkillDetailLoadingKey(null);
})
.catch(() => {
if (!clientRef.current) {
setSkillDetailLoadingKey(null);
return;
}
console.debug('REST skill detail request failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'get_skill_detail', agent_id: selectedSkillAgentId, skill_name: normalized });
if (!success) {
setSkillDetailLoadingKey(null);
}
});
return true;
}, [clientRef, resolveRunId, selectedSkillAgentId, setSkillDetailLoadingKey, setSkillDetailsByName]);
const handleCreateLocalSkill = useCallback((skillName) => {
const normalized = typeof skillName === 'string' ? skillName.trim() : '';
if (!normalized) {
setAgentSkillsFeedback({ type: 'error', text: '技能名称不能为空' });
return;
}
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${normalized}:create`);
setAgentSkillsFeedback(null);
void resolveRunId()
.then((runId) => createAgentLocalSkill(runId, selectedSkillAgentId, normalized))
.then(() => {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'success', text: `已创建本地技能 ${normalized}` });
requestAgentSkills(selectedSkillAgentId);
requestSkillDetail(normalized);
})
.catch(() => {
if (!clientRef.current) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
console.debug('REST local skill create failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'create_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: normalized });
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
});
}, [clientRef, requestAgentSkills, requestSkillDetail, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
const handleLocalSkillDraftChange = useCallback((skillName, content) => {
const detailKey = `${selectedSkillAgentId}:${skillName}`;
useAgentStore.getState().setLocalSkillDraftsByKey((prev) => ({ ...prev, [detailKey]: content }));
}, [selectedSkillAgentId]);
const handleLocalSkillSave = useCallback((skillName) => {
const detailKey = `${selectedSkillAgentId}:${skillName}`;
const content = localSkillDraftsByKey[detailKey];
if (typeof content !== 'string') return;
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:content`);
setAgentSkillsFeedback(null);
void resolveRunId()
.then((runId) => updateAgentLocalSkill(runId, selectedSkillAgentId, skillName, content))
.then(() => {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 的本地技能 ${skillName} 已保存` });
requestSkillDetail(skillName);
})
.catch(() => {
if (!clientRef.current) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
console.debug('REST local skill save failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'update_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: skillName, content });
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
});
}, [clientRef, localSkillDraftsByKey, requestSkillDetail, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
const handleLocalSkillDelete = useCallback((skillName) => {
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:delete`);
setAgentSkillsFeedback(null);
void resolveRunId()
.then((runId) => deleteAgentLocalSkill(runId, selectedSkillAgentId, skillName))
.then(() => {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 的本地技能 ${skillName} 已删除` });
requestAgentSkills(selectedSkillAgentId);
})
.catch(() => {
if (!clientRef.current) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
console.debug('REST local skill delete failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'delete_agent_local_skill', agent_id: selectedSkillAgentId, skill_name: skillName });
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
});
}, [clientRef, requestAgentSkills, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
const handleRemoveSharedSkill = useCallback((skillName) => {
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:remove`);
setAgentSkillsFeedback(null);
void resolveRunId()
.then((runId) => disableAgentSkill(runId, selectedSkillAgentId, skillName))
.then(() => {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'success', text: `${selectedSkillAgentId} 已移除共享技能 ${skillName}` });
requestAgentSkills(selectedSkillAgentId);
})
.catch(() => {
if (!clientRef.current) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
console.debug('REST shared skill remove failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'remove_agent_skill', agent_id: selectedSkillAgentId, skill_name: skillName });
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
});
}, [clientRef, requestAgentSkills, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
const handleAgentSkillToggle = useCallback((skillName, enabled) => {
const agentId = selectedSkillAgentId;
setAgentSkillsSavingKey(`${agentId}:${skillName}`);
setAgentSkillsFeedback(null);
void resolveRunId()
.then((runId) => enabled
? enableAgentSkill(runId, agentId, skillName)
: disableAgentSkill(runId, agentId, skillName))
.then(() => {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'success', text: `${agentId} ${enabled ? '已启用' : '已禁用'} ${skillName}` });
requestAgentSkills(agentId);
})
.catch(() => {
if (!clientRef.current) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
console.debug('REST skill toggle failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'update_agent_skill', agent_id: agentId, skill_name: skillName, enabled });
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
});
}, [clientRef, requestAgentSkills, resolveRunId, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
const handleSkillAgentChange = useCallback((agentId) => {
setSelectedSkillAgentId(agentId);
requestAgentProfile(agentId);
requestAgentSkills(agentId);
requestWorkspaceFile(agentId, selectedWorkspaceFile);
}, [requestAgentProfile, requestAgentSkills, setSelectedSkillAgentId, selectedWorkspaceFile]);
const requestWorkspaceFile = useCallback((agentId, filename) => {
const normalizedAgentId = typeof agentId === 'string' ? agentId.trim() : '';
const normalizedFilename = typeof filename === 'string' ? filename.trim() : '';
if (!normalizedAgentId || !normalizedFilename) return false;
setIsWorkspaceFileLoading(true);
setWorkspaceFileFeedback(null);
void resolveRunId()
.then((runId) => fetchAgentWorkspaceFile(runId, normalizedAgentId, normalizedFilename))
.then((payload) => {
setWorkspaceFilesByAgent((prev) => ({
...prev,
[normalizedAgentId]: {
...(prev[normalizedAgentId] || {}),
[normalizedFilename]: typeof payload?.content === 'string' ? payload.content : ''
}
}));
setWorkspaceDraftContent(typeof payload?.content === 'string' ? payload.content : '');
setIsWorkspaceFileLoading(false);
})
.catch(() => {
if (!clientRef.current) {
setIsWorkspaceFileLoading(false);
return;
}
console.debug('REST workspace file read failed, falling back to websocket compatibility path');
const success = clientRef.current.send({ type: 'get_agent_workspace_file', agent_id: normalizedAgentId, filename: normalizedFilename });
if (!success) {
setIsWorkspaceFileLoading(false);
}
});
return true;
}, [clientRef, resolveRunId, setIsWorkspaceFileLoading, setWorkspaceDraftContent, setWorkspaceFileFeedback, setWorkspaceFilesByAgent]);
const handleWorkspaceFileChange = useCallback((filename) => {
useAgentStore.getState().setSelectedWorkspaceFile(filename);
requestWorkspaceFile(selectedSkillAgentId, filename);
}, [requestWorkspaceFile, selectedSkillAgentId]);
const handleWorkspaceFileSave = useCallback(() => {
const key = `${selectedSkillAgentId}:${selectedWorkspaceFile}`;
setWorkspaceFileSavingKey(key);
setWorkspaceFileFeedback(null);
void resolveRunId()
.then((runId) => updateAgentWorkspaceFile(runId, selectedSkillAgentId, selectedWorkspaceFile, workspaceDraftContent))
.then((payload) => {
setWorkspaceFileSavingKey(null);
setWorkspaceFileFeedback({ type: 'success', text: `${selectedSkillAgentId}${selectedWorkspaceFile} 已保存` });
setWorkspaceFilesByAgent((prev) => ({
...prev,
[selectedSkillAgentId]: {
...(prev[selectedSkillAgentId] || {}),
[selectedWorkspaceFile]: typeof payload?.content === 'string' ? payload.content : workspaceDraftContent
}
}));
})
.catch(() => {
if (!clientRef.current) {
setWorkspaceFileSavingKey(null);
setWorkspaceFileFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
console.debug('REST workspace file save failed, falling back to websocket compatibility path');
const success = clientRef.current.send({
type: 'update_agent_workspace_file',
agent_id: selectedSkillAgentId,
filename: selectedWorkspaceFile,
content: workspaceDraftContent
});
if (!success) {
setWorkspaceFileSavingKey(null);
setWorkspaceFileFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
});
}, [clientRef, resolveRunId, selectedSkillAgentId, selectedWorkspaceFile, setWorkspaceFileFeedback, setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, workspaceDraftContent]);
const handleUploadExternalSkill = useCallback(async (file) => {
if (!(file instanceof File)) {
setAgentSkillsFeedback({ type: 'error', text: '请选择 zip 文件后再上传' });
return;
}
if (!selectedSkillAgentId) {
setAgentSkillsFeedback({ type: 'error', text: '未选择目标 Agent' });
return;
}
setAgentSkillsSavingKey(`${selectedSkillAgentId}:__upload__`);
setAgentSkillsFeedback(null);
try {
const result = await uploadAgentSkillZip({ agentId: selectedSkillAgentId, file, activate: true });
setAgentSkillsFeedback({ type: 'success', text: `已上传并安装技能 ${result.skill_name || ''}`.trim() });
requestAgentSkills(selectedSkillAgentId);
} catch (error) {
setAgentSkillsFeedback({ type: 'error', text: `上传失败: ${error.message || '未知错误'}` });
} finally {
setAgentSkillsSavingKey(null);
}
}, [requestAgentSkills, selectedSkillAgentId, setAgentSkillsFeedback, setAgentSkillsSavingKey]);
return {
requestAgentSkills,
requestAgentProfile,
requestSkillDetail,
handleCreateLocalSkill,
handleLocalSkillDraftChange,
handleLocalSkillSave,
handleLocalSkillDelete,
handleRemoveSharedSkill,
handleAgentSkillToggle,
handleSkillAgentChange,
requestWorkspaceFile,
handleWorkspaceFileChange,
handleWorkspaceFileSave,
handleUploadExternalSkill
};
}