Add dynamic analyst runtime updates and deployment guides

This commit is contained in:
2026-04-07 09:39:37 +08:00
parent 80ce63da5a
commit 62c7341cf6
45 changed files with 1886 additions and 159 deletions

BIN
frontend/public/media/0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
frontend/public/media/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
frontend/public/media/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
frontend/public/media/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
frontend/public/media/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
frontend/public/media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
frontend/public/media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
frontend/public/media/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
frontend/public/media/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
frontend/public/media/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -8,12 +8,17 @@ import { useFeedProcessor } from './hooks/useFeedProcessor';
import { useRuntimeControls } from './hooks/useRuntimeControls';
import { useStockDataRequests } from './hooks/useStockDataRequests';
import { useWebSocketConnection } from './hooks/useWebSocketConnection';
import { fetchRuntimeLogs } from './services/runtimeApi';
import { fetchRuntimeAgents, fetchRuntimeLogs } from './services/runtimeApi';
import { useAgentRunFileState, useAgentStore } from './store/agentStore';
import { useMarketStore } from './store/marketStore';
import { usePortfolioStore } from './store/portfolioStore';
import { useRuntimeStore } from './store/runtimeStore';
import { useUIStore } from './store/uiStore';
import {
buildRuntimeAgentMeta,
findAgentByIdOrName,
sortRuntimeAgents,
} from './utils/agentDisplay';
const EDITABLE_AGENT_WORKSPACE_FILES = [
'SOUL.md',
@@ -174,21 +179,57 @@ export default function LiveTradingApp() {
const [isRuntimeLogsLoading, setIsRuntimeLogsLoading] = useState(false);
const [runtimeLogsPayload, setRuntimeLogsPayload] = useState(null);
const [runtimeLogsError, setRuntimeLogsError] = useState(null);
const [runtimeAgents, setRuntimeAgents] = useState([]);
const agentFeedRef = useRef(null);
const isSocketReady = isConnected && connectionStatus === 'connected';
const selectedAgentId = selectedSkillAgentId || AGENTS[0]?.id || null;
const resolvedAgents = useMemo(() => {
if (!Array.isArray(runtimeAgents) || runtimeAgents.length === 0) {
return AGENTS;
}
return sortRuntimeAgents(runtimeAgents).map((agentState, index) => {
const agentId = String(agentState?.agent_id || agentState?.id || '').trim();
const base = buildRuntimeAgentMeta(agentId, index);
const displayName = typeof agentState?.display_name === 'string' ? agentState.display_name.trim() : '';
return {
...base,
id: agentId,
name: displayName || base.name,
runtimeStatus: agentState?.status || null,
lastSession: agentState?.last_session || null,
lastUpdated: agentState?.last_updated || null,
};
}).filter((agent) => agent.id);
}, [runtimeAgents]);
const selectedAgentId = selectedSkillAgentId || resolvedAgents[0]?.id || null;
const selectedAgentProfile = selectedAgentId ? (agentProfilesByAgent[selectedAgentId] || null) : null;
const selectedAgentSkills = selectedAgentId ? (agentSkillsByAgent[selectedAgentId] || []) : [];
const selectedRunFileContent = selectedAgentId && selectedRunFile
? (runFilesByAgent[selectedAgentId]?.[selectedRunFile] || '')
: '';
useEffect(() => {
if (!selectedSkillAgentId && AGENTS.length > 0) {
setSelectedSkillAgentId(AGENTS[0].id);
const loadRuntimeAgentsList = useCallback(async () => {
try {
const payload = await fetchRuntimeAgents();
setRuntimeAgents(Array.isArray(payload?.agents) ? payload.agents : []);
} catch {
setRuntimeAgents([]);
}
}, [selectedSkillAgentId, setSelectedSkillAgentId]);
}, []);
useEffect(() => {
if (!selectedSkillAgentId && resolvedAgents.length > 0) {
setSelectedSkillAgentId(resolvedAgents[0].id);
}
}, [resolvedAgents, selectedSkillAgentId, setSelectedSkillAgentId]);
useEffect(() => {
if (selectedSkillAgentId && !resolvedAgents.some((agent) => agent.id === selectedSkillAgentId)) {
setSelectedSkillAgentId(resolvedAgents[0]?.id || null);
}
}, [resolvedAgents, selectedSkillAgentId, setSelectedSkillAgentId]);
useEffect(() => {
if (!selectedRunFile) {
@@ -196,6 +237,37 @@ export default function LiveTradingApp() {
}
}, [selectedRunFile, setSelectedWorkspaceFile]);
useEffect(() => {
void loadRuntimeAgentsList();
}, [loadRuntimeAgentsList]);
useEffect(() => {
const handleRuntimeAgentsUpdated = () => {
void loadRuntimeAgentsList();
};
window.addEventListener('runtime-agents-updated', handleRuntimeAgentsUpdated);
return () => {
window.removeEventListener('runtime-agents-updated', handleRuntimeAgentsUpdated);
};
}, [loadRuntimeAgentsList]);
useEffect(() => {
if (!isSocketReady) {
return;
}
void loadRuntimeAgentsList();
}, [isSocketReady, loadRuntimeAgentsList]);
useEffect(() => {
if (!selectedAgentId || !selectedRunFile) {
setRunDraftContent('');
return;
}
const cachedContent = runFilesByAgent[selectedAgentId]?.[selectedRunFile];
setRunDraftContent(typeof cachedContent === 'string' ? cachedContent : '');
}, [runFilesByAgent, selectedAgentId, selectedRunFile, setRunDraftContent]);
useEffect(() => {
if (!isSocketReady || !selectedAgentId || !clientRef.current) {
return;
@@ -233,7 +305,7 @@ export default function LiveTradingApp() {
return;
}
AGENTS.forEach((agent) => {
resolvedAgents.forEach((agent) => {
if (!agent?.id) {
return;
}
@@ -246,6 +318,7 @@ export default function LiveTradingApp() {
clientRef,
isSocketReady,
requestAgentProfile,
resolvedAgents,
]);
useEffect(() => {
@@ -326,13 +399,13 @@ export default function LiveTradingApp() {
const bubbleFor = useCallback((idOrName) => {
let bubble = bubbles[idOrName];
if (bubble) return bubble;
const agent = AGENTS.find((item) => item.name === idOrName || item.id === idOrName);
const agent = findAgentByIdOrName(resolvedAgents, idOrName);
if (agent) {
bubble = bubbles[agent.id];
if (bubble) return bubble;
}
return null;
}, [bubbles]);
}, [bubbles, resolvedAgents]);
const handleManualTrigger = useCallback(() => {
if (!isSocketReady || !clientRef.current) {
@@ -361,7 +434,7 @@ export default function LiveTradingApp() {
}, []);
const agentRequests = {
agents: AGENTS,
agents: resolvedAgents,
agentProfilesByAgent,
agentSkillsByAgent,
runFilesByAgent,

View File

@@ -1,9 +1,10 @@
import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react';
import { formatTime } from '../utils/formatters';
import { MESSAGE_COLORS, getAgentColors, AGENTS, ASSETS } from '../config/constants';
import { MESSAGE_COLORS, getAgentColors, ASSETS } from '../config/constants';
import { getModelIcon } from '../utils/modelIcons';
import MarkdownModal from './MarkdownModal';
import LobeModelLogo from './LobeModelLogo.jsx';
import { findAgentByIdOrName, humanizeAgentId } from '../utils/agentDisplay';
const isAnalyst = (agentId, agentName) => {
if (agentId && agentId.includes('analyst')) return true;
@@ -36,7 +37,7 @@ const stripMarkdown = (text) => {
.replace(/^[-=]+$/gm, '');
};
const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref) => {
const AgentFeed = forwardRef(({ agents = [], feed, leaderboard, agentProfilesByAgent }, ref) => {
const feedContentRef = useRef(null);
const [highlightedId, setHighlightedId] = useState(null);
const [selectedAgent, setSelectedAgent] = useState('all');
@@ -62,7 +63,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
// Get agent info by name
const getAgentInfoByName = (agentName) => {
if (!agentName) return null;
const agentConfig = AGENTS.find((agent) => agent.name === agentName);
const agentConfig = findAgentByIdOrName(agents, agentName);
const profile = agentConfig ? agentProfilesByAgent?.[agentConfig.id] : null;
if (agentConfig && profile?.model_name) {
return {
@@ -81,7 +82,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
};
};
// Get unique agent names from feed (only registered agents in AGENTS)
// Get unique agent names from feed using the current runtime agent list.
const getUniqueAgents = () => {
const agentNamesInFeed = new Set();
@@ -98,9 +99,10 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
}
});
// Filter to only include registered agents and sort by AGENTS array order
const registeredAgentNames = AGENTS.map(a => a.name);
return registeredAgentNames.filter(name => agentNamesInFeed.has(name));
const orderedRuntimeNames = agents.map((agent) => agent.name);
const knownNames = orderedRuntimeNames.filter(name => agentNamesInFeed.has(name));
const extraNames = [...agentNamesInFeed].filter(name => !orderedRuntimeNames.includes(name));
return [...knownNames, ...extraNames];
};
// Filter feed based on selected agent
@@ -177,6 +179,12 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
const currentSelection = getCurrentSelectionInfo();
const resolveAgentDisplayName = (name, agentId) => {
if (name) return name;
const agent = findAgentByIdOrName(agents, agentId);
return agent?.name || humanizeAgentId(agentId);
};
return (
<div className="agent-feed">
<div className="agent-feed-header">
@@ -241,7 +249,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
type="color"
/>
)}
<span>{agent}</span>
<span>{resolveAgentDisplayName(agent, agentInfo?.agentId)}</span>
</div>
);
})}
@@ -255,7 +263,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
<div className="empty-state">
{selectedAgent === 'all'
? '等待系统更新...'
: `${selectedAgent} 没有消息`}
: `${resolveAgentDisplayName(selectedAgent, currentSelection.agentInfo?.agentId)} 没有消息`}
</div>
)}

View File

@@ -3,7 +3,6 @@ import GlobalStyles from '../styles/GlobalStyles';
import Header from './Header.jsx';
import RuntimeSettingsPanel from './RuntimeSettingsPanel.jsx';
import NetValueChart from './NetValueChart.jsx';
import { AGENTS } from '../config/constants';
import { useRuntimeStore } from '../store/runtimeStore';
import { useUIStore } from '../store/uiStore';
import { formatNumber, formatTickerPrice } from '../utils/formatters';
@@ -401,6 +400,7 @@ export default function AppShell({
<div className="view-panel">
<Suspense fallback={<ViewLoadingFallback label="加载交易室..." />}>
<RoomView
agents={agentRequests.agents}
bubbles={bubbles}
bubbleFor={bubbleFor}
leaderboard={leaderboard}
@@ -501,7 +501,7 @@ export default function AppShell({
{/* Right Panel: Agent Feed */}
<div className="right-panel" style={{ width: `${100 - leftWidth}%` }}>
<Suspense fallback={<ViewLoadingFallback label="加载消息流..." />}>
<AgentFeed ref={agentFeedRef} feed={feed} leaderboard={leaderboard} agentProfilesByAgent={agentProfilesByAgent} />
<AgentFeed ref={agentFeedRef} agents={agentRequests.agents} feed={feed} leaderboard={leaderboard} agentProfilesByAgent={agentProfilesByAgent} />
</Suspense>
</div>
</div>

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { ASSETS, SCENE_NATIVE, AGENT_SEATS, AGENTS } from '../config/constants';
import { ASSETS, SCENE_NATIVE, AGENT_SEATS } from '../config/constants';
import AgentCard from './AgentCard';
import { getModelIcon } from '../utils/modelIcons';
import LobeModelLogo from './LobeModelLogo.jsx';
import { findAgentByIdOrName } from '../utils/agentDisplay';
/**
* Custom hook to load an image
@@ -48,7 +49,22 @@ 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, agentProfilesByAgent, feed, onJumpToMessage, onOpenLaunchConfig }) {
function getSeatPosition(index) {
if (AGENT_SEATS[index]) {
return AGENT_SEATS[index];
}
const overflowIndex = index - AGENT_SEATS.length;
const columns = 3;
const row = Math.floor(overflowIndex / columns);
const column = overflowIndex % columns;
return {
x: 0.18 + (column * 0.18),
y: Math.max(0.14, 0.22 - (row * 0.1)),
};
}
export default function RoomView({ agents = [], bubbles, bubbleFor, leaderboard, agentProfilesByAgent, feed, onJumpToMessage, onOpenLaunchConfig }) {
const canvasRef = useRef(null);
const containerRef = useRef(null);
@@ -152,16 +168,16 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
// Determine which agents are speaking
const speakingAgents = useMemo(() => {
const speaking = {};
AGENTS.forEach(agent => {
agents.forEach(agent => {
const bubble = bubbleFor(agent.name);
speaking[agent.id] = !!bubble;
});
return speaking;
}, [bubbles, bubbleFor]);
}, [agents, bubbleFor, bubbles]);
// Find agent data from leaderboard
const getAgentData = (agentId) => {
const agent = AGENTS.find(a => a.id === agentId);
const agent = agents.find(a => a.id === agentId);
if (!agent) return null;
const profile = agentProfilesByAgent?.[agentId] || null;
@@ -195,7 +211,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
};
}
// Merge data but preserve the correct avatar from AGENTS config
// Merge data but preserve the configured visual metadata from frontend.
return {
...agent,
...leaderboardData,
@@ -317,10 +333,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
// Skip system messages
if (msg.agent === 'System') return;
// Find matching agent
const agent = AGENTS.find(a =>
a.id === msg.agentId ||
a.name === msg.agent
);
const agent = findAgentByIdOrName(agents, msg.agentId || msg.agent);
if (agent) {
messages.push({
feedItemId: item.id,
@@ -333,10 +346,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
} else if (item.type === 'conference' && item.data?.messages) {
item.data.messages.forEach((msg, msgIndex) => {
if (msg.agent === 'System') return;
const agent = AGENTS.find(a =>
a.id === msg.agentId ||
a.name === msg.agent
);
const agent = findAgentByIdOrName(agents, msg.agentId || msg.agent);
if (agent) {
messages.push({
feedItemId: item.id,
@@ -479,7 +489,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
if (isReplaying) {
// Find replay bubble for this agent
const bubble = Object.values(replayBubbles).find(b => {
const agent = AGENTS.find(a => a.id === b.agentId);
const agent = agents.find(a => a.id === b.agentId);
return agent && agent.name === agentName;
});
return bubble || null;
@@ -487,13 +497,13 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
// Use normal bubbleFor function
return bubbleFor(agentName);
}
}, [isReplaying, replayBubbles, bubbleFor]);
}, [agents, isReplaying, replayBubbles, bubbleFor]);
return (
<div className="room-view">
{/* Agents Indicator Bar */}
<div className="room-agents-indicator">
{AGENTS.map((agent, index) => {
{agents.map((agent, index) => {
const rank = getAgentRank(agent.id);
const medal = rank ? getRankMedal(rank) : null;
const agentData = getAgentData(agent.id);
@@ -572,7 +582,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
<canvas ref={canvasRef} className="room-canvas" />
{/* Speech Bubbles */}
{AGENTS.map((agent, idx) => {
{agents.map((agent, idx) => {
const bubble = getBubbleForAgent(agent.name);
if (!bubble) return null;
@@ -581,7 +591,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
// Check if bubble is hidden
if (hiddenBubbles[bubbleKey]) return null;
const pos = AGENT_SEATS[idx];
const pos = getSeatPosition(idx);
const scaledWidth = SCENE_NATIVE.width * scale;
const scaledHeight = SCENE_NATIVE.height * scale;

View File

@@ -3,6 +3,7 @@
*/
const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
const mediaAsset = (filename) => `/media/${filename}`;
const isLocalDevHost = () => {
if (typeof window === "undefined") {
return false;
@@ -14,12 +15,12 @@ const isLocalDevHost = () => {
// Centralized CDN asset URLs
export const CDN_ASSETS = {
companyRoom: {
agent_1: "https://img.alicdn.com/imgextra/i4/O1CN01Lr7SOl1lSExV0tOwv_!!6000000004817-2-tps-370-320.png",
agent_2: "https://img.alicdn.com/imgextra/i3/O1CN017Kb8cY1VQNUmuK47o_!!6000000002647-2-tps-368-312.png",
agent_3: "https://img.alicdn.com/imgextra/i3/O1CN010Fp55w1YqtGpVjgsS_!!6000000003111-2-tps-370-320.png",
agent_4: "https://img.alicdn.com/imgextra/i3/O1CN01VnUsML1Dkq6fHw3ks_!!6000000000255-2-tps-366-316.png",
agent_5: "https://img.alicdn.com/imgextra/i4/O1CN01o0kCQw1kyvbulBSl7_!!6000000004753-2-tps-370-314.png",
agent_6: "https://img.alicdn.com/imgextra/i2/O1CN01cLV0zl1FI6ULAunTp_!!6000000000463-2-tps-368-320.png",
agent_1: mediaAsset("0.png"),
agent_2: mediaAsset("1.png"),
agent_3: mediaAsset("2.png"),
agent_4: mediaAsset("3.png"),
agent_5: mediaAsset("4.png"),
agent_6: mediaAsset("5.png"),
team_logo: "https://img.alicdn.com/imgextra/i2/O1CN01n2S8aV25hcZhhNH95_!!6000000007558-2-tps-616-700.png",
reme_logo: "https://img.alicdn.com/imgextra/i2/O1CN01FhncuT1Tqp8LfCaft_!!6000000002434-2-tps-915-250.png",
full_room_dark: "https://img.alicdn.com/imgextra/i2/O1CN014sOgzK28re5haGC3X_!!6000000007986-2-tps-1248-832.png",
@@ -45,6 +46,14 @@ export const ASSETS = {
remeLogo: CDN_ASSETS.companyRoom.reme_logo,
};
export const NON_MANAGER_AVATAR_POOL = Array.from({ length: 10 }, (_, index) => (
mediaAsset(`${index + 2}.png`)
));
export const DYNAMIC_ANALYST_AVATAR_POOL = Array.from({ length: 6 }, (_, index) => (
mediaAsset(`${index + 6}.png`)
));
// Scene dimensions (actual image size)
export const SCENE_NATIVE = { width: 1248, height: 832 };
@@ -383,4 +392,3 @@ export const suggestAgentId = (name, baseType) => {
// Must end with '_analyst' to get analysis tools registered
return `${normalized || baseType}_${timestamp}_analyst`;
};

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef } from "react";
import { AGENTS } from "../config/constants";
import { humanizeAgentId } from "../utils/agentDisplay";
const MAX_FEED_ITEMS = 200;
@@ -108,7 +109,7 @@ const eventToMessage = (evt) => {
id: generateId("msg"),
timestamp,
agentId: evt.agentId,
agent: normalizeAgentLabel(agent?.name || evt.agentName || evt.agentId || "Agent", evt.agentId),
agent: normalizeAgentLabel(agent?.name || evt.agentName || humanizeAgentId(evt.agentId) || "Agent", evt.agentId),
role: agent?.role || evt.role || "Agent",
content: evt.content
};
@@ -118,7 +119,7 @@ const eventToMessage = (evt) => {
id: generateId("memory"),
timestamp,
agentId: evt.agentId,
agent: agent?.name || evt.agentId || "Memory",
agent: agent?.name || humanizeAgentId(evt.agentId) || "Memory",
role: "Memory",
content: evt.content || evt.text || ""
};

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useCallback } from 'react';
import { AGENTS } from '../config/constants';
import { fetchRuntimeAgents } from '../services/runtimeApi';
import { ReadOnlyClient } from '../services/websocket';
import { useRuntimeStore } from '../store/runtimeStore';
import { useOpenClawStore } from '../store/openclawStore';
@@ -8,6 +9,7 @@ import { usePortfolioStore } from '../store/portfolioStore';
import { useAgentStore } from '../store/agentStore';
import { useUIStore } from '../store/uiStore';
import { normalizeTickerSymbols } from '../services/runtimeControls';
import { humanizeAgentId } from '../utils/agentDisplay';
/**
* Normalize price history from server format
@@ -401,7 +403,7 @@ export function useWebSocketConnection({
setLocalSkillDraftsByKey, setIsAgentSkillsLoading, setSkillDetailLoadingKey,
setAgentSkillsSavingKey, setAgentSkillsFeedback, setIsWorkspaceFileLoading,
setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, setWorkspaceFileFeedback,
selectedSkillAgentId } = useAgentStore();
setWorkspaceDraftContent, selectedSkillAgentId } = useAgentStore();
const { setBubbles } = useUIStore();
@@ -705,14 +707,19 @@ export function useWebSocketConnection({
agent_workspace_file_loaded: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const filename = typeof e.filename === 'string' ? e.filename.trim() : '';
const content = typeof e.content === 'string' ? e.content : '';
if (!agentId || !filename) {
setIsWorkspaceFileLoading(false);
return;
}
setWorkspaceFilesByAgent((prev) => ({
...prev,
[agentId]: { ...(prev[agentId] || {}), [filename]: typeof e.content === 'string' ? e.content : '' }
[agentId]: { ...(prev[agentId] || {}), [filename]: content }
}));
const { selectedSkillAgentId: currentAgentId, selectedWorkspaceFile: currentFilename } = useAgentStore.getState();
if (currentAgentId === agentId && currentFilename === filename) {
setWorkspaceDraftContent(content);
}
setIsWorkspaceFileLoading(false);
setWorkspaceFileSavingKey(null);
},
@@ -1018,16 +1025,25 @@ export function useWebSocketConnection({
agent_message: (e) => {
const agent = AGENTS.find((item) => item.id === e.agentId);
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || e.agentId } });
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || humanizeAgentId(e.agentId) } });
processFeedEvent(e);
},
conference_message: (e) => {
const agent = AGENTS.find((item) => item.id === e.agentId);
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || e.agentId } });
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || humanizeAgentId(e.agentId) } });
processFeedEvent(e);
},
runtime_agents_updated: async () => {
try {
await fetchRuntimeAgents();
window.dispatchEvent(new CustomEvent('runtime-agents-updated'));
} catch {
// Ignore refresh failures; next manual/runtime refresh will recover.
}
},
memory: (e) => processFeedEvent(e),
team_summary: (e) => {

View File

@@ -1,4 +1,5 @@
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
const resolveValue = (updater, currentValue) => (
typeof updater === 'function' ? updater(currentValue) : updater
@@ -66,13 +67,15 @@ export const useAgentStore = create((set) => ({
* 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,
}));
export const useAgentRunFileState = () => useAgentStore(
useShallow((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,
}))
);

View File

@@ -0,0 +1,77 @@
import { AGENTS, DYNAMIC_ANALYST_AVATAR_POOL } from '../config/constants';
export const STATIC_AGENT_INDEX = new Map(AGENTS.map((agent, index) => [agent.id, { agent, index }]));
const ANALYST_COLOR_PALETTE = AGENTS.filter((agent) => agent.id.endsWith('_analyst')).map((agent) => agent.colors);
const FALLBACK_AGENT_VISUALS = DYNAMIC_ANALYST_AVATAR_POOL.map((avatar, index) => {
return {
avatar,
colors: ANALYST_COLOR_PALETTE[index % Math.max(ANALYST_COLOR_PALETTE.length, 1)] || AGENTS[0].colors,
};
});
export function humanizeAgentId(agentId) {
if (!agentId) return '未知 Agent';
const normalized = String(agentId).trim();
const staticAgent = STATIC_AGENT_INDEX.get(normalized)?.agent;
if (staticAgent?.name) {
return staticAgent.name;
}
const label = normalized
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase());
return label || normalized;
}
export function inferAgentRole(agentId) {
const normalized = String(agentId || '').trim();
const staticAgent = STATIC_AGENT_INDEX.get(normalized)?.agent;
if (staticAgent?.role) {
return staticAgent.role;
}
if (normalized === 'portfolio_manager') return '投资经理';
if (normalized === 'risk_manager') return '风控经理';
if (normalized.endsWith('_analyst')) return '分析师';
return 'Agent';
}
export function buildRuntimeAgentMeta(agentId, runtimeIndex = 0) {
const normalized = String(agentId || '').trim();
const staticAgent = STATIC_AGENT_INDEX.get(normalized)?.agent;
if (staticAgent) {
return staticAgent;
}
const fallback = FALLBACK_AGENT_VISUALS[runtimeIndex % Math.max(FALLBACK_AGENT_VISUALS.length, 1)] || {
avatar: AGENTS[0].avatar,
colors: AGENTS[0].colors,
};
return {
id: normalized,
name: humanizeAgentId(normalized),
role: inferAgentRole(normalized),
avatar: fallback.avatar,
colors: fallback.colors,
};
}
export function sortRuntimeAgents(agents) {
const staticOrder = new Map(AGENTS.map((agent, index) => [agent.id, index]));
return [...agents].sort((left, right) => {
const leftId = String(left?.agent_id || left?.id || '').trim();
const rightId = String(right?.agent_id || right?.id || '').trim();
const leftStatic = staticOrder.has(leftId) ? staticOrder.get(leftId) : Number.MAX_SAFE_INTEGER;
const rightStatic = staticOrder.has(rightId) ? staticOrder.get(rightId) : Number.MAX_SAFE_INTEGER;
if (leftStatic !== rightStatic) {
return leftStatic - rightStatic;
}
return leftId.localeCompare(rightId);
});
}
export function findAgentByIdOrName(agents, idOrName) {
if (!Array.isArray(agents) || !idOrName) {
return null;
}
return agents.find((agent) => agent.id === idOrName || agent.name === idOrName) || null;
}