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:
2026-04-02 00:55:08 +08:00
parent 0fa413380c
commit 16b54d5ccc
73 changed files with 9454 additions and 904 deletions

View File

@@ -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,
};

View File

@@ -208,7 +208,7 @@ export default function RuntimeSettingsPanel({
background: '#FFFFFF',
border: '1px dashed #D0D7DE'
}}>
恢复启动会从所选历史任务复制运行状态组合交易记录和 Agent 工作区资产并以新的任务 ID 继续运行
恢复启动会从所选历史任务复制运行状态组合交易记录和 Agent 运行资产并以新的任务 ID 继续运行
</div>
</>
)}

View File

@@ -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={{

View File

@@ -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>

View File

@@ -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)) {

View 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;

View File

@@ -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

View 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'
})
);
});
});

View File

@@ -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,
}));