import { useEffect, useRef, useCallback } from 'react'; import { AGENTS } from '../config/constants'; import { ReadOnlyClient } from '../services/websocket'; import { useRuntimeStore } from '../store/runtimeStore'; import { useOpenClawStore } from '../store/openclawStore'; import { useMarketStore } from '../store/marketStore'; import { usePortfolioStore } from '../store/portfolioStore'; import { useAgentStore } from '../store/agentStore'; import { useUIStore } from '../store/uiStore'; import { normalizeTickerSymbols } from '../services/runtimeControls'; /** * Normalize price history from server format */ function normalizePriceHistory(payload) { if (!payload || typeof payload !== 'object') { return {}; } const normalized = {}; Object.entries(payload).forEach(([symbol, points]) => { const ticker = String(symbol || '').trim().toUpperCase(); if (!ticker || !Array.isArray(points)) { return; } normalized[ticker] = points .map((point) => { if (Array.isArray(point) && point.length >= 2) { const [label, value] = point; const price = Number(value); if (!label || !Number.isFinite(price)) return null; return { timestamp: String(label), label: String(label), price }; } if (point && typeof point === 'object') { const rawTimestamp = point.timestamp ?? point.t ?? point.date ?? point.label; const price = Number(point.price ?? point.v ?? point.value ?? point.close); if (!rawTimestamp || !Number.isFinite(price)) return null; return { timestamp: String(rawTimestamp), label: String(rawTimestamp), price }; } return null; }) .filter(Boolean) .slice(-120); }); return normalized; } /** * Build tickers from symbols array */ function buildTickersFromSymbols(symbols, previousTickers = []) { if (!Array.isArray(symbols) || symbols.length === 0) { return previousTickers; } return symbols .filter((symbol) => typeof symbol === 'string' && symbol.trim()) .map((symbol) => { const normalized = symbol.trim().toUpperCase(); const existing = previousTickers.find((ticker) => ticker.symbol === normalized); return existing || { symbol: normalized, price: null, change: null }; }); } function normalizeOpenClawHistoryItems(history) { if (!Array.isArray(history)) { return []; } return history .map((item, index) => { const role = item?.role || item?.senderRole || item?.kind || item?.type || 'event'; const isFinal = hasOpenClawFinalTag(item); const text = extractOpenClawText(item); if (!shouldKeepOpenClawMessage(item)) { return null; } const timestamp = item?.timestamp || item?.ts || item?.createdAt || item?.time || null; const nestedMeta = item?.message?.__openclaw || item?.__openclaw || null; const seq = item?.messageSeq ?? item?.seq ?? nestedMeta?.seq ?? null; const messageId = item?.messageId ?? item?.id ?? nestedMeta?.id ?? null; return { id: messageId || (seq !== null ? `seq:${seq}` : `${timestamp || 'history'}:${index}`), role, text: String(text || ''), timestamp, seq, messageId, isFinal, raw: item, }; }) .filter(Boolean); } function unwrapOpenClawFinal(value) { if (typeof value !== 'string') { return null; } const match = value.match(/([\s\S]*?)<\/final>/i); if (!match) { return null; } return match[1].trim(); } function stripOpenClawFinalTags(value) { if (typeof value !== 'string') { return value ? String(value) : ''; } return value.replace(/<\/?final>/gi, '').trim(); } function shouldHideOpenClawMessage({ role, text }) { const normalizedRole = String(role || '').toLowerCase(); const normalizedText = String(text || '').trim(); if (normalizedRole === 'system') { return true; } if (normalizedRole === 'user') { if (normalizedText.startsWith('Sender (untrusted metadata):')) { return true; } if (normalizedText.startsWith('[Fri ') || normalizedText.startsWith('[Sat ') || normalizedText.startsWith('[Sun ') || normalizedText.startsWith('[Mon ') || normalizedText.startsWith('[Tue ') || normalizedText.startsWith('[Wed ') || normalizedText.startsWith('[Thu ')) { return true; } } return false; } function shouldKeepOpenClawMessage(item) { const role = item?.role || item?.senderRole || item?.kind || item?.type || 'event'; const text = extractOpenClawText(item); const isFinal = hasOpenClawFinalTag(item); if (shouldHideOpenClawMessage({ role, text })) { return false; } const normalizedRole = String(role || '').toLowerCase(); if (normalizedRole === 'assistant') { return isFinal; } if (!normalizedRole || normalizedRole === 'event') { return isFinal; } return true; } function hasOpenClawFinalTag(item) { if (typeof item === 'string') { return /[\s\S]*?<\/final>/i.test(item); } if (!item || typeof item !== 'object') { return false; } const candidates = []; if (typeof item.text === 'string') candidates.push(item.text); if (typeof item.message === 'string') candidates.push(item.message); if (typeof item.content === 'string') candidates.push(item.content); const nestedMessage = item.message && typeof item.message === 'object' ? item.message : null; if (nestedMessage) { if (typeof nestedMessage.content === 'string') candidates.push(nestedMessage.content); if (Array.isArray(nestedMessage.content)) { nestedMessage.content.forEach((entry) => { if (typeof entry === 'string') candidates.push(entry); if (entry?.type === 'text' && typeof entry?.text === 'string') candidates.push(entry.text); }); } } if (Array.isArray(item.content)) { item.content.forEach((entry) => { if (typeof entry === 'string') candidates.push(entry); if (entry?.type === 'text' && typeof entry?.text === 'string') candidates.push(entry.text); }); } return candidates.some((value) => /[\s\S]*?<\/final>/i.test(value)); } function extractOpenClawText(item) { if (typeof item === 'string') { return unwrapOpenClawFinal(item) || stripOpenClawFinalTags(item); } if (!item || typeof item !== 'object') { return item ? String(item) : ''; } if (typeof item.text === 'string' && item.text.trim()) { return unwrapOpenClawFinal(item.text) || stripOpenClawFinalTags(item.text); } if (typeof item.message === 'string' && item.message.trim()) { return unwrapOpenClawFinal(item.message) || stripOpenClawFinalTags(item.message); } if (typeof item.content === 'string' && item.content.trim()) { return unwrapOpenClawFinal(item.content) || stripOpenClawFinalTags(item.content); } const nestedMessage = item.message && typeof item.message === 'object' ? item.message : null; if (nestedMessage) { if (typeof nestedMessage.content === 'string' && nestedMessage.content.trim()) { return unwrapOpenClawFinal(nestedMessage.content) || stripOpenClawFinalTags(nestedMessage.content); } if (Array.isArray(nestedMessage.content)) { const textBlock = nestedMessage.content.find((entry) => entry?.type === 'text' && typeof entry?.text === 'string'); if (textBlock?.text) { return unwrapOpenClawFinal(textBlock.text) || stripOpenClawFinalTags(textBlock.text); } } } if (Array.isArray(item.content)) { const textParts = item.content .map((entry) => { if (typeof entry === 'string') return entry; if (entry?.type === 'text' && typeof entry?.text === 'string') return entry.text; return ''; }) .filter(Boolean); if (textParts.length > 0) { const merged = textParts.join('\n'); return unwrapOpenClawFinal(merged) || stripOpenClawFinalTags(merged); } } if (typeof item.summary === 'string' && item.summary.trim()) { return item.summary; } if (typeof item.value === 'string' && item.value.trim()) { return item.value; } return JSON.stringify(item); } function normalizeOpenClawLiveEvent(evt) { const payload = evt?.payload || {}; const nestedMessage = payload?.message && typeof payload.message === 'object' ? payload.message : null; const nestedMeta = nestedMessage?.__openclaw || payload?.__openclaw || null; const isFinal = hasOpenClawFinalTag(payload); const text = extractOpenClawText(payload) || evt?.event || ''; const role = payload.role || nestedMessage?.role || payload.senderRole || payload.kind || evt?.event || 'event'; const seq = payload.messageSeq ?? payload.seq ?? nestedMeta?.seq ?? null; const messageId = payload.messageId ?? payload.id ?? nestedMeta?.id ?? null; return { id: messageId || (seq !== null ? `seq:${seq}` : `${evt?.event || 'event'}:${Date.now()}`), role, text: String(text), timestamp: payload.timestamp || payload.ts || new Date().toISOString(), seq, messageId, isFinal, raw: payload, }; } function shouldAppendOpenClawLiveEvent(evt) { const name = String(evt?.event || ''); const payload = evt?.payload || {}; if (name === 'session.message') { return shouldKeepOpenClawMessage(payload); } return Boolean(payload.text || payload.message || payload.content); } function requestOpenClawSessionHistory(clientRef, sessionKey, limit = 30) { const client = clientRef?.current; if (!client || !sessionKey) { return false; } return client.send(JSON.stringify({ type: 'get_openclaw_session_history', session_key: sessionKey, limit, })); } function normalizeOpenClawAgents(agents, presence, sessionsPayload = null) { const normalizedAgents = Array.isArray(agents) ? agents : []; const presenceAgents = presence?.agents || presence || {}; const sessionDefaults = sessionsPayload?.defaults || {}; const sessions = Array.isArray(sessionsPayload?.sessions) ? sessionsPayload.sessions : []; const sessionModelByAgent = new Map(); sessions.forEach((session) => { if (!session || typeof session !== 'object') return; let agentId = String(session.agentId || session.agent_id || '').trim(); if (!agentId) { const key = String(session.key || session.sessionKey || '').trim(); const parts = key.split(':'); if (parts.length >= 3 && parts[0] === 'agent') { agentId = parts[1]; } } const modelValue = session.model || session.modelName || session.model_name || session.resolvedModel || session.resolved_model || null; if (agentId && modelValue && !sessionModelByAgent.has(agentId)) { sessionModelByAgent.set(agentId, modelValue); } }); return normalizedAgents.map((agent) => { if (!agent || typeof agent !== 'object') { return agent; } const agentId = String(agent.id || agent.agentId || '').trim(); const presenceEntry = agentId ? presenceAgents?.[agentId] : null; const presenceSessions = Array.isArray(presenceEntry?.sessions) ? presenceEntry.sessions : []; const firstPresenceSession = presenceSessions.find((session) => { const value = session?.model || session?.modelName || session?.model_name || session?.resolvedModel; return typeof value === 'string' && value.trim(); }); const model = agent.model || agent.modelName || agent.model_name || agent.resolvedModel || agent.resolved_model || agent.defaultModel || agent.default_model || sessionModelByAgent.get(agentId) || sessionDefaults.model || sessionDefaults.modelName || sessionDefaults.model_name || firstPresenceSession?.model || firstPresenceSession?.modelName || firstPresenceSession?.model_name || firstPresenceSession?.resolvedModel || null; return { ...agent, model: typeof model === 'string' && model.trim() ? model.trim() : null, }; }); } /** * Custom hook for WebSocket connection lifecycle and event handling. * Manages clientRef, connection, and ALL event handlers. * Feeds directly into stores (no props drilling). */ export function useWebSocketConnection({ processHistoricalFeed, processFeedEvent, addSystemMessage }) { const clientRef = useRef(null); const isWatchlistSavingRef = useRef(false); const isRuntimeConfigSavingRef = useRef(false); const selectedSkillAgentIdRef = useRef(null); const requestedStockHistoryRef = useRef(new Set()); // Store state const { setIsConnected, setConnectionStatus, setSystemStatus, setCurrentDate, setServerMode, setDataSources, setRuntimeConfig, setMarketStatus, setVirtualTime, setProgress, watchlistDraftSymbols, setWatchlistInputValue, setIsWatchlistSaving, setWatchlistFeedback, setIsRuntimeConfigSaving, setRuntimeConfigFeedback, isWatchlistSaving, isRuntimeConfigSaving, setLastDayHistory } = useRuntimeStore(); const { tickers, setTickers, setRollingTickers, setPriceHistoryByTicker, setExplainEventsByTicker, setNewsByTicker, setInsiderTradesByTicker, setTechnicalIndicatorsByTicker, setHistorySourceByTicker, setOhlcHistoryByTicker } = useMarketStore(); const { setPortfolioData, setHoldings, setTrades, setStats, setLeaderboard } = usePortfolioStore(); const { setAgentSkillsByAgent, setAgentProfilesByAgent, setSkillDetailsByName, setLocalSkillDraftsByKey, setIsAgentSkillsLoading, setSkillDetailLoadingKey, setAgentSkillsSavingKey, setAgentSkillsFeedback, setIsWorkspaceFileLoading, setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, setWorkspaceFileFeedback, selectedSkillAgentId } = useAgentStore(); const { setBubbles } = useUIStore(); // Helper: Update tickers from realtime prices const updateTickersFromPrices = useCallback((realtimePrices) => { try { setTickers((prevTickers) => prevTickers.map((ticker) => { const realtimeData = realtimePrices[ticker.symbol]; if (realtimeData && realtimeData.price !== null && realtimeData.price !== undefined) { const newChange = (realtimeData.ret !== null && realtimeData.ret !== undefined) ? realtimeData.ret : (ticker.change !== null && ticker.change !== undefined ? ticker.change : 0); return { ...ticker, price: realtimeData.price, change: newChange, open: realtimeData.open || ticker.open }; } return ticker; })); } catch (error) { console.error('Error updating tickers from prices:', error); } }, [setTickers]); // Stock request callbacks (these will be provided by useStockDataRequests) const requestStockHistoryRef = useRef(null); const requestStockNewsTimelineRef = useRef(null); const requestStockNewsCategoriesRef = useRef(null); const setRequestStockHistory = useCallback((fn) => { requestStockHistoryRef.current = fn; }, []); const setRequestStockNewsTimeline = useCallback((fn) => { requestStockNewsTimelineRef.current = fn; }, []); const setRequestStockNewsCategories = useCallback((fn) => { requestStockNewsCategoriesRef.current = fn; }, []); useEffect(() => { const handlePushEvent = (evt) => { if (!evt) return; try { handleEventInternal(evt); } catch (error) { console.error('[Event Handler] Error:', error); } }; const handleEventInternal = (evt) => { if (evt?.type && evt.type !== 'pong') { setConnectionStatus('connected'); setIsConnected(true); } const handlers = { error: (e) => { const message = typeof e.message === 'string' ? e.message : '请求失败'; console.error('[Error]', message); setIsAgentSkillsLoading(false); setSkillDetailLoadingKey(null); setAgentSkillsSavingKey(null); setIsWorkspaceFileLoading(false); setWorkspaceFileSavingKey(null); if (isWatchlistSavingRef.current) { setIsWatchlistSaving(false); setWatchlistFeedback({ type: 'error', text: message || '更新 watchlist 失败' }); } if (isRuntimeConfigSavingRef.current) { setIsRuntimeConfigSaving(false); setRuntimeConfigFeedback({ type: 'error', text: message }); } if (message.includes('skill') || message.includes('agent_id')) { setAgentSkillsFeedback({ type: 'error', text: message || '更新技能失败' }); } if (message.includes('workspace_file') || message.includes('filename')) { setWorkspaceFileFeedback({ type: 'error', text: message || '更新工作区文件失败' }); } if (message.includes('fast forward')) { console.warn(`⚠️ ${message}`); handlePushEvent({ type: 'system', content: `⚠️ ${message}`, timestamp: Date.now() }); } addSystemMessage(message); }, system: (e) => { console.log('[System]', e.content); if (e.content.includes('Connected') || e.content.includes('已连接')) { setConnectionStatus('connected'); setIsConnected(true); } else if (e.content.includes('Disconnected') || e.content.includes('断开')) { setConnectionStatus('disconnected'); setIsConnected(false); } processFeedEvent(e); }, pong: () => { console.log('[Heartbeat] Pong received'); }, initial_state: (e) => { try { const state = e.state; if (!state) return; setConnectionStatus('connected'); setIsConnected(true); setSystemStatus(state.status || 'initializing'); setCurrentDate(state.current_date); if (state.server_mode) setServerMode(state.server_mode); if (state.data_sources) setDataSources(state.data_sources); if (state.runtime_config) setRuntimeConfig(state.runtime_config); if (Array.isArray(state.tickers) && state.tickers.length > 0) { setTickers((prevTickers) => buildTickersFromSymbols(state.tickers, prevTickers)); } if (state.market_status) { setMarketStatus(state.market_status); setVirtualTime(null); } if (state.trading_days_total) { setProgress({ current: state.trading_days_completed || 0, total: state.trading_days_total }); } if (state.portfolio) { setPortfolioData((prev) => ({ ...prev, netValue: state.portfolio.total_value || prev.netValue, pnl: state.portfolio.pnl_percent || 0, equity: state.portfolio.equity || prev.equity, baseline: state.portfolio.baseline || prev.baseline, baseline_vw: state.portfolio.baseline_vw || prev.baseline_vw, momentum: state.portfolio.momentum || prev.momentum, strategies: state.portfolio.strategies || prev.strategies, equity_return: state.portfolio.equity_return || prev.equity_return, baseline_return: state.portfolio.baseline_return || prev.baseline_return, baseline_vw_return: state.portfolio.baseline_vw_return || prev.baseline_vw_return, momentum_return: state.portfolio.momentum_return || prev.momentum_return })); } if (state.dashboard) { if (state.dashboard.holdings) setHoldings(state.dashboard.holdings); if (state.dashboard.trades) setTrades(state.dashboard.trades); if (state.dashboard.stats) setStats(state.dashboard.stats); if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard); } if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices); if (state.price_history) { setPriceHistoryByTicker(normalizePriceHistory(state.price_history)); } if (state.feed_history && Array.isArray(state.feed_history)) { console.log(`✅ Loading ${state.feed_history.length} historical events`); processHistoricalFeed(state.feed_history); } if (state.last_day_history && Array.isArray(state.last_day_history)) { setLastDayHistory(state.last_day_history); console.log(`✅ Loaded ${state.last_day_history.length} last day events for replay`); } console.log('Initial state loaded'); } catch (error) { console.error('Error loading initial state:', error); } }, market_status_update: (e) => { if (e.market_status) setMarketStatus(e.market_status); }, data_sources_update: (e) => { if (e.data_sources) setDataSources(e.data_sources); }, runtime_assets_reloaded: (e) => { if (e.runtime_config_applied) setRuntimeConfig(e.runtime_config_applied); if (Array.isArray(e.runtime_config_applied?.tickers)) { setTickers((prevTickers) => buildTickersFromSymbols(e.runtime_config_applied.tickers, prevTickers) ); setWatchlistInputValue(''); } if (isWatchlistSavingRef.current) setIsWatchlistSaving(false); if (isRuntimeConfigSavingRef.current) { setIsRuntimeConfigSaving(false); setRuntimeConfigFeedback({ type: 'success', text: '运行配置已保存并生效' }); } const warnings = Array.isArray(e.runtime_config_warnings) ? e.runtime_config_warnings : []; warnings.forEach((warning) => addSystemMessage(warning)); addSystemMessage('运行时配置已热更新'); }, agent_skills_loaded: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; if (!agentId) { setIsAgentSkillsLoading(false); return; } setAgentSkillsByAgent((prev) => ({ ...prev, [agentId]: Array.isArray(e.skills) ? e.skills : [] })); setIsAgentSkillsLoading(false); setAgentSkillsSavingKey(null); }, agent_profile_loaded: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; if (!agentId) return; setAgentProfilesByAgent((prev) => ({ ...prev, [agentId]: e.profile && typeof e.profile === 'object' ? e.profile : {} })); }, skill_detail_loaded: (e) => { const skillName = typeof e.skill?.skill_name === 'string' ? e.skill.skill_name.trim() : ''; const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : selectedSkillAgentIdRef.current; if (!skillName) { setSkillDetailLoadingKey(null); return; } const detailKey = `${agentId}:${skillName}`; setSkillDetailsByName((prev) => ({ ...prev, [detailKey]: e.skill })); setLocalSkillDraftsByKey((prev) => ({ ...prev, [detailKey]: typeof e.skill?.content === 'string' ? e.skill.content : '' })); setSkillDetailLoadingKey(null); }, agent_skill_updated: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : ''; if (!agentId || !skillName) return; setAgentSkillsFeedback({ type: 'success', text: `${agentId} ${e.enabled ? '已启用' : '已禁用'} ${skillName}` }); }, agent_local_skill_created: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : ''; setAgentSkillsSavingKey(null); if (!agentId || !skillName) return; setAgentSkillsFeedback({ type: 'success', text: `${agentId} 已创建本地技能 ${skillName}` }); }, agent_local_skill_updated: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : ''; setAgentSkillsSavingKey(null); if (!agentId || !skillName) return; setAgentSkillsFeedback({ type: 'success', text: `${agentId} 的本地技能 ${skillName} 已保存` }); }, agent_local_skill_deleted: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : ''; setAgentSkillsSavingKey(null); if (!agentId || !skillName) return; setSkillDetailsByName((prev) => { const next = { ...prev }; delete next[`${agentId}:${skillName}`]; return next; }); setLocalSkillDraftsByKey((prev) => { const next = { ...prev }; delete next[`${agentId}:${skillName}`]; return next; }); setAgentSkillsFeedback({ type: 'success', text: `${agentId} 的本地技能 ${skillName} 已删除` }); }, agent_skill_removed: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : ''; setAgentSkillsSavingKey(null); if (!agentId || !skillName) return; setAgentSkillsFeedback({ type: 'success', text: `${agentId} 已移除共享技能 ${skillName}` }); }, 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() : ''; if (!agentId || !filename) { setIsWorkspaceFileLoading(false); return; } setWorkspaceFilesByAgent((prev) => ({ ...prev, [agentId]: { ...(prev[agentId] || {}), [filename]: typeof e.content === 'string' ? e.content : '' } })); setIsWorkspaceFileLoading(false); setWorkspaceFileSavingKey(null); }, agent_workspace_file_updated: (e) => { const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : ''; const filename = typeof e.filename === 'string' ? e.filename.trim() : ''; if (!agentId || !filename) return; setWorkspaceFileFeedback({ type: 'success', text: `${agentId} 的 ${filename} 已保存` }); }, watchlist_updated: (e) => { if (Array.isArray(e.tickers)) { const normalizedTickers = e.tickers.map((symbol) => String(symbol).trim().toUpperCase()); setRuntimeConfig((prev) => ({ ...(prev || {}), tickers: normalizedTickers })); setTickers((prevTickers) => buildTickersFromSymbols(normalizedTickers, prevTickers)); } setIsWatchlistSaving(false); setWatchlistFeedback({ type: 'success', text: `已更新为 ${Array.isArray(e.tickers) ? e.tickers.join(', ') : '最新列表'}` }); }, stock_history_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; if (Array.isArray(e.prices)) { setOhlcHistoryByTicker((prev) => ({ ...prev, [symbol]: e.prices })); setHistorySourceByTicker((prev) => ({ ...prev, [symbol]: e.source || null })); } }, stock_explain_events_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; setExplainEventsByTicker((prev) => ({ ...prev, [symbol]: { events: Array.isArray(e.events) ? e.events : [], signals: Array.isArray(e.signals) ? e.signals : [], trades: Array.isArray(e.trades) ? e.trades : [] } })); }, stock_news_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), items: Array.isArray(e.news) ? e.news : [], source: e.source || null, startDate: e.start_date || null, endDate: e.end_date || null, freshness: e.freshness || null } })); if (requestStockNewsTimelineRef.current) requestStockNewsTimelineRef.current(symbol); }, stock_news_for_date_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; const date = typeof e.date === 'string' ? e.date.trim() : ''; if (!symbol || !date) return; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), byDate: { ...((prev[symbol] && prev[symbol].byDate) || {}), [date]: Array.isArray(e.news) ? e.news : [] }, byDateFreshness: { ...((prev[symbol] && prev[symbol].byDateFreshness) || {}), [date]: e.freshness || null } } })); }, stock_news_timeline_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), timeline: Array.isArray(e.timeline) ? e.timeline : [], timelineStartDate: e.start_date || null, timelineEndDate: e.end_date || null, timelineFreshness: e.freshness || null } })); }, stock_news_categories_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), categories: e.categories || {}, categoriesStartDate: e.start_date || null, categoriesEndDate: e.end_date || null, categoriesFreshness: e.freshness || null } })); }, stock_insider_trades_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; setInsiderTradesByTicker((prev) => ({ ...prev, [symbol]: { trades: Array.isArray(e.trades) ? e.trades : [], startDate: e.start_date || null, endDate: e.end_date || null } })); }, stock_technical_indicators_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; setTechnicalIndicatorsByTicker((prev) => ({ ...prev, [symbol]: e.indicators || null })); }, stock_range_explain_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; const result = e.result && typeof e.result === 'object' ? e.result : null; if (!result?.start_date || !result?.end_date) return; const cacheKey = `${result.start_date}:${result.end_date}`; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), rangeExplainCache: { ...((prev[symbol] && prev[symbol].rangeExplainCache) || {}), [cacheKey]: { ...result, freshness: e.freshness || null } } } })); }, stock_story_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; const asOfDate = typeof e.as_of_date === 'string' ? e.as_of_date.trim() : ''; if (!symbol || !asOfDate) return; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), storyCache: { ...((prev[symbol] && prev[symbol].storyCache) || {}), [asOfDate]: { story: e.story || '', source: e.source || null, asOfDate, freshness: e.freshness || null } } } })); }, stock_similar_days_loaded: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; const date = typeof e.target_date === 'string' ? e.target_date.trim() : typeof e.date === 'string' ? e.date.trim() : ''; if (!symbol || !date) return; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), similarDaysCache: { ...((prev[symbol] && prev[symbol].similarDaysCache) || {}), [date]: { target_features: e.target_features || {}, items: Array.isArray(e.items) ? e.items : [], error: e.error || null, freshness: e.freshness || null } } } })); }, stock_enrich_completed: (e) => { const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : ''; if (!symbol) return; const completedAt = new Date().toISOString(); const historyEntry = { timestamp: completedAt, startDate: e.start_date || '', endDate: e.end_date || '', force: Boolean(e.force), onlyLocalToLlm: Boolean(e.only_local_to_llm), error: e.error || null, stats: e.stats || null, storyStatus: e.story_status || null, similarStatus: e.similar_status || null }; setNewsByTicker((prev) => ({ ...prev, [symbol]: { ...(prev[symbol] || {}), items: [], byDate: {}, timeline: [], categories: {}, rangeExplainCache: {}, storyCache: {}, similarDaysCache: {}, maintenanceStatus: { running: false, error: e.error || null, updatedAt: completedAt, stats: e.stats || null, storyStatus: e.story_status || null, similarStatus: e.similar_status || null }, maintenanceHistory: [historyEntry, ...(((prev[symbol] && prev[symbol].maintenanceHistory) || []).slice(0, 7))] } })); if (!e.error) { if (requestStockHistoryRef.current) requestStockHistoryRef.current(symbol); if (requestStockNewsTimelineRef.current) requestStockNewsTimelineRef.current(symbol); if (requestStockNewsCategoriesRef.current) requestStockNewsCategoriesRef.current(symbol); } }, price_update: (e) => { try { const { symbol, price, ret, open, portfolio, realtime_prices } = e; if (!symbol || !price) { console.warn('[Price Update] Missing symbol or price:', e); return; } setConnectionStatus('connected'); setIsConnected(true); console.log(`[Price Update] ${symbol}: $${price} (ret: ${ret !== undefined ? ret.toFixed(2) : 'N/A'}%)`); setPriceHistoryByTicker((prev) => { const ticker = String(symbol).trim().toUpperCase(); const nextPoint = { timestamp: new Date().toISOString(), label: new Date().toISOString(), price: Number(price) }; const existing = Array.isArray(prev[ticker]) ? prev[ticker] : []; const lastPoint = existing[existing.length - 1]; if (lastPoint && Number(lastPoint.price) === Number(nextPoint.price)) return prev; return { ...prev, [ticker]: [...existing, nextPoint].slice(-120) }; }); const normalizedSymbol = String(symbol).trim().toUpperCase(); let shouldAnimateTicker = false; setTickers((prevTickers) => prevTickers.map((ticker) => { if (ticker.symbol === symbol) { const oldPrice = ticker.price; let newChange = ticker.change; if (ret !== null && ret !== undefined) { newChange = ret; } else if (oldPrice !== null && oldPrice !== undefined && isFinite(oldPrice)) { const priceChange = ((price - oldPrice) / oldPrice) * 100; newChange = (newChange !== null && newChange !== undefined) ? newChange + priceChange : priceChange; } else { newChange = 0; } if (oldPrice !== price) shouldAnimateTicker = true; return { ...ticker, price, change: newChange, open: open || ticker.open }; } return ticker; })); if (shouldAnimateTicker) { setRollingTickers((prev) => ({ ...prev, [normalizedSymbol]: true })); setTimeout(() => setRollingTickers((prev) => ({ ...prev, [normalizedSymbol]: false })), 500); } if (realtime_prices) updateTickersFromPrices(realtime_prices); if (portfolio && portfolio.total_value) { setPortfolioData((prev) => ({ ...prev, netValue: portfolio.total_value, pnl: portfolio.pnl_percent || 0, equity: portfolio.equity || prev.equity })); } } catch (error) { console.error('[Price Update] Error:', error); } }, day_start: (e) => { setCurrentDate(e.date); if (e.progress !== undefined) { setProgress((prev) => ({ ...prev, current: Math.floor(e.progress * (prev.total || 1)) })); } setSystemStatus('running'); processFeedEvent(e); }, day_complete: (e) => { const result = e.result; if (result && typeof result === 'object') { if (result.portfolio_summary) { const summary = result.portfolio_summary; setPortfolioData((prev) => { const newEquity = [...prev.equity]; const dateObj = new Date(e.date); newEquity.push({ t: dateObj.getTime(), v: summary.total_value || summary.cash || prev.netValue }); return { ...prev, netValue: summary.total_value || summary.cash || prev.netValue, pnl: summary.pnl_percent || 0, equity: newEquity }; }); } } processFeedEvent(e); }, day_error: (e) => { console.error('Day error:', e.date, e.error); processFeedEvent(e); }, conference_start: (e) => processFeedEvent(e), conference_end: (e) => processFeedEvent(e), 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 } }); 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 } }); processFeedEvent(e); }, memory: (e) => processFeedEvent(e), team_summary: (e) => { setPortfolioData((prev) => ({ ...prev, netValue: e.balance || prev.netValue, pnl: e.pnlPct || 0, equity: e.equity || prev.equity, baseline: e.baseline || prev.baseline, baseline_vw: e.baseline_vw || prev.baseline_vw, momentum: e.momentum || prev.momentum, equity_return: e.equity_return || prev.equity_return, baseline_return: e.baseline_return || prev.baseline_return, baseline_vw_return: e.baseline_vw_return || prev.baseline_vw_return, momentum_return: e.momentum_return || prev.momentum_return })); }, team_portfolio: (e) => { if (e.holdings) setHoldings(e.holdings); }, team_holdings: (e) => { if (e.data && Array.isArray(e.data)) { setHoldings(e.data); console.log(`✅ Holdings updated: ${e.data.length} positions`); } }, team_trades: (e) => { if (e.mode === 'full' && e.data && Array.isArray(e.data)) { setTrades(e.data); } else if (Array.isArray(e.trades)) { setTrades(e.trades); } else if (e.trade) { setTrades((prev) => [e.trade, ...prev].slice(0, 100)); } }, team_stats: (e) => { if (e.data) setStats(e.data); else if (e.stats) setStats(e.stats); }, team_leaderboard: (e) => { if (Array.isArray(e.data)) setLeaderboard(e.data); else if (Array.isArray(e.rows)) setLeaderboard(e.rows); else if (Array.isArray(e.leaderboard)) setLeaderboard(e.leaderboard); }, time_update: (e) => { if (e.beijing_time_str) { const statusEmoji = { market_open: '📊', off_market: '⏸️', non_trading_day: '📅', trade_execution: '💼' }; const emoji = statusEmoji[e.status] || '⏰'; let logMessage = `${emoji} 时间: ${e.beijing_time_str} | 状态: ${e.status}`; if (e.hours_to_open !== undefined) logMessage += ` | 距离开盘: ${e.hours_to_open}小时`; if (e.hours_to_trade !== undefined) logMessage += ` | 距离交易: ${e.hours_to_trade}小时`; if (e.trading_date) logMessage += ` | 交易日: ${e.trading_date}`; console.log(logMessage); setVirtualTime(null); } if (e.market_status) setMarketStatus(e.market_status); }, time_fast_forwarded: (e) => { console.log(`⏩ 时间已快进 ${e.minutes} 分钟: ${e.old_time_str} → ${e.new_time_str}`); if (e.new_time) { try { setVirtualTime(new Date(e.new_time)); handlePushEvent({ type: 'system', content: `⏩ 时间快进 ${e.minutes} 分钟: ${e.old_time_str} → ${e.new_time_str}`, timestamp: Date.now() }); } catch (error) { console.error('Error parsing fast forwarded time:', error); } } }, fast_forward_success: (e) => { console.log(`✅ ${e.message}`); }, openclaw_status_loaded: (e) => { useOpenClawStore.getState().setOpenclawStatus(e.data || e); useOpenClawStore.getState().setStatusLoading(false); }, openclaw_sessions_loaded: (e) => { const payload = e.data || e; useOpenClawStore.getState().setOpenclawSessions(payload); const currentAgents = useOpenClawStore.getState().agents || []; const presence = useOpenClawStore.getState().agentsPresence; if (currentAgents.length > 0) { useOpenClawStore.getState().setAgents( normalizeOpenClawAgents(currentAgents, presence, payload), ); } useOpenClawStore.getState().setSessionsLoading(false); }, openclaw_session_detail_loaded: (e) => { useOpenClawStore.getState().setOpenclawSessionDetail(e.data || e); useOpenClawStore.getState().setSessionDetailLoading(false); }, openclaw_session_history_loaded: (e) => { const data = e.data || e; const sessionKey = e.session_key || data?.session_key || useOpenClawStore.getState().selectedSessionKey; useOpenClawStore.getState().setOpenclawSessionHistory(data); if (sessionKey) { useOpenClawStore.getState().replaceOpenclawChatHistory( sessionKey, normalizeOpenClawHistoryItems(data?.history || []), ); } }, openclaw_session_resolved: (e) => { const d = e.data || {}; useOpenClawStore.getState().setOpenclawResolvedSessionKey(d.key || null); if (d?.error) { useOpenClawStore.getState().setChatError(d.error); } else { useOpenClawStore.getState().setChatError(null); } }, openclaw_session_created: (e) => { const d = e.data || {}; if (d?.error) { useOpenClawStore.getState().setChatError(d.error); return; } if (d?.entry || d?.key) { const createdKey = d?.key || d?.entry?.key || d?.entry?.sessionKey || ''; useOpenClawStore.getState().appendOpenclawSession( d.entry || { key: createdKey, sessionKey: createdKey, agentId: String(createdKey).split(':')[1] || '', } ); } if (d?.key) { useOpenClawStore.getState().setSelectedSessionKey(d.key); useOpenClawStore.getState().setOpenclawResolvedSessionKey(d.key); useOpenClawStore.getState().setChatError(null); } }, openclaw_session_subscribed: (e) => { const sessionKey = e.session_key || e.data?.key || null; if (sessionKey) { useOpenClawStore.getState().setOpenclawSessionSubscribed(sessionKey, true); } if (e.data?.error) { useOpenClawStore.getState().setChatError(e.data.error); } }, openclaw_session_unsubscribed: (e) => { const sessionKey = e.session_key || e.data?.key || null; if (sessionKey) { useOpenClawStore.getState().setOpenclawSessionSubscribed(sessionKey, false); } }, openclaw_session_reset: (e) => { const sessionKey = e.session_key || e.data?.key || null; if (e.data?.error) { useOpenClawStore.getState().setChatError(e.data.error); return; } if (sessionKey) { useOpenClawStore.getState().replaceOpenclawChatHistory(sessionKey, []); useOpenClawStore.getState().setChatError(null); } }, openclaw_session_deleted: (e) => { const sessionKey = e.session_key || e.data?.key || null; if (e.data?.error) { useOpenClawStore.getState().setChatError(e.data.error); return; } if (sessionKey) { useOpenClawStore.getState().removeOpenclawSession(sessionKey); useOpenClawStore.getState().setChatError(null); } }, openclaw_message_sent: (e) => { const sessionKey = e.session_key || e.data?.key || useOpenClawStore.getState().selectedSessionKey; if (sessionKey) { useOpenClawStore.getState().setOpenclawChatSendingForSession(sessionKey, false); } if (e.data?.error) { useOpenClawStore.getState().setChatError(e.data.error); } else { useOpenClawStore.getState().setChatError(null); if (sessionKey && (e.data?.status || e.data?.runId || e.data?.messageSeq !== undefined)) { const statusBits = [ e.data?.status ? `status=${e.data.status}` : null, e.data?.runId ? `runId=${e.data.runId}` : null, e.data?.messageSeq !== undefined ? `seq=${e.data.messageSeq}` : null, ].filter(Boolean); useOpenClawStore.getState().appendOpenclawChatMessage(sessionKey, { id: `send-meta:${e.data?.runId || Date.now()}`, role: 'system', text: `消息已提交到 OpenClaw${statusBits.length ? ` (${statusBits.join(', ')})` : ''}`, timestamp: new Date().toISOString(), }); } if (sessionKey) { window.setTimeout(() => requestOpenClawSessionHistory(clientRef, sessionKey, 30), 600); } } }, openclaw_session_event: (e) => { const sessionKey = e.session_key || e.payload?.sessionKey || e.payload?.key; if (!sessionKey || !shouldAppendOpenClawLiveEvent(e)) { return; } useOpenClawStore.getState().appendOpenclawChatMessage( sessionKey, normalizeOpenClawLiveEvent(e), ); }, openclaw_cron_loaded: (e) => { useOpenClawStore.getState().setOpenclawCronJobs(e.data || e); useOpenClawStore.getState().setCronLoading(false); }, openclaw_approvals_loaded: (e) => { useOpenClawStore.getState().setOpenclawApprovals(e.data || e); useOpenClawStore.getState().setApprovalsLoading(false); }, openclaw_agents_loaded: (e) => { useOpenClawStore.getState().setAgentsLoading(false); const d = e.data?.data ?? e.data; if (d?.error) { useOpenClawStore.getState().setAgentsError(d.error); } else { const presence = useOpenClawStore.getState().agentsPresence; const sessionsPayload = { sessions: useOpenClawStore.getState().openclawSessions || [], defaults: useOpenClawStore.getState().openclawSessionsDefaults || null, }; useOpenClawStore.getState().setAgents( normalizeOpenClawAgents(d?.agents || [], presence, sessionsPayload), ); useOpenClawStore.getState().setAgentsError(null); } }, openclaw_agents_presence_loaded: (e) => { const presencePayload = (e.data?.data ?? e.data) || {}; useOpenClawStore.getState().setAgentsPresence(presencePayload); const currentAgents = useOpenClawStore.getState().agents || []; if (currentAgents.length > 0) { const sessionsPayload = { sessions: useOpenClawStore.getState().openclawSessions || [], defaults: useOpenClawStore.getState().openclawSessionsDefaults || null, }; useOpenClawStore.getState().setAgents( normalizeOpenClawAgents(currentAgents, presencePayload, sessionsPayload), ); } }, openclaw_skills_loaded: (e) => { useOpenClawStore.getState().setSkillsLoading(false); const d = e.data?.data ?? e.data; if (d?.error) { useOpenClawStore.getState().setSkillsError(d.error); } else { useOpenClawStore.getState().setSkills(d?.skills || []); useOpenClawStore.getState().setSkillsError(null); } }, openclaw_models_loaded: (e) => { useOpenClawStore.getState().setModelsLoading(false); const d = e.data?.data ?? e.data; if (d?.error) { useOpenClawStore.getState().setModelsError(d.error); } else { useOpenClawStore.getState().setModels(d?.models || []); useOpenClawStore.getState().setModelsError(null); } }, openclaw_workspace_files_loaded: (e) => { useOpenClawStore.getState().setWorkspaceFilesLoading(false); const d = e.data?.data ?? e.data; const workspace = d?.workspace || ""; if (d?.error) { useOpenClawStore.getState().setWorkspaceFilesError(d.error); } else { useOpenClawStore.getState().setWorkspaceFiles(workspace, d); useOpenClawStore.getState().setWorkspaceFilesError(null); } }, openclaw_workspace_file_loaded: (e) => { const d = e.data?.data ?? e.data; console.log("[DEBUG] workspace_file_loaded:", { d }); if (d?.error) return; const agentId = d?.agentId || "main"; const fileName = d?.file?.Name || d?.file?.name || ""; const key = `${agentId}:${fileName}`; if (d?.file?.missing) { useOpenClawStore.getState().setWorkspaceFileContent(key, "(文件不存在)"); } else if (d?.file?.content) { useOpenClawStore.getState().setWorkspaceFileContent(key, d.file.content); } }, openclaw_hooks_loaded: (e) => { useOpenClawStore.getState().setHooksLoading(false); const d = e.data?.data ?? e.data; if (d?.error) { useOpenClawStore.getState().setHooksError(d.error); } else { useOpenClawStore.getState().setHooks(d?.hooks || []); useOpenClawStore.getState().setHooksError(null); } }, openclaw_plugins_loaded: (e) => { useOpenClawStore.getState().setPluginsLoading(false); const d = e.data?.data ?? e.data; if (d?.error) { useOpenClawStore.getState().setPluginsError(d.error); } else { useOpenClawStore.getState().setPlugins(d?.plugins || []); useOpenClawStore.getState().setPluginsError(null); } }, openclaw_secrets_audit_loaded: (e) => { useOpenClawStore.getState().setSecretsAuditLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setSecretsAuditError(e.data.data.error); } else { useOpenClawStore.getState().setSecretsAudit(e.data?.data || null); useOpenClawStore.getState().setSecretsAuditError(null); } }, openclaw_security_audit_loaded: (e) => { useOpenClawStore.getState().setSecurityAuditLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setSecurityAuditError(e.data.data.error); } else { useOpenClawStore.getState().setSecurityAudit(e.data?.data || null); useOpenClawStore.getState().setSecurityAuditError(null); } }, openclaw_daemon_status_loaded: (e) => { useOpenClawStore.getState().setDaemonStatusLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setDaemonStatusError(e.data.data.error); } else { useOpenClawStore.getState().setDaemonStatus(e.data?.data || null); useOpenClawStore.getState().setDaemonStatusError(null); } }, openclaw_pairing_loaded: (e) => { useOpenClawStore.getState().setPairingLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setPairingError(e.data.data.error); } else { useOpenClawStore.getState().setPairing(e.data?.data || null); useOpenClawStore.getState().setPairingError(null); } }, openclaw_qr_loaded: (e) => { useOpenClawStore.getState().setQrCodeLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setQrCodeError(e.data.data.error); } else { useOpenClawStore.getState().setQrCode(e.data?.data || null); useOpenClawStore.getState().setQrCodeError(null); } }, openclaw_update_status_loaded: (e) => { useOpenClawStore.getState().setUpdateStatusLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setUpdateStatusError(e.data.data.error); } else { useOpenClawStore.getState().setUpdateStatus(e.data?.data || null); useOpenClawStore.getState().setUpdateStatusError(null); } }, openclaw_models_aliases_loaded: (e) => { useOpenClawStore.getState().setModelsAliasesLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setModelsAliasesError(e.data.data.error); } else { useOpenClawStore.getState().setModelsAliases(e.data?.data || null); useOpenClawStore.getState().setModelsAliasesError(null); } }, openclaw_models_fallbacks_loaded: (e) => { useOpenClawStore.getState().setModelsFallbacksLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setModelsFallbacksError(e.data.data.error); } else { useOpenClawStore.getState().setModelsFallbacks(e.data?.data?.items || []); useOpenClawStore.getState().setModelsFallbacksError(null); } }, openclaw_models_image_fallbacks_loaded: (e) => { useOpenClawStore.getState().setModelsImageFallbacksLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setModelsImageFallbacksError(e.data.data.error); } else { useOpenClawStore.getState().setModelsImageFallbacks(e.data?.data?.items || []); useOpenClawStore.getState().setModelsImageFallbacksError(null); } }, openclaw_skill_update_loaded: (e) => { useOpenClawStore.getState().setSkillUpdateLoading(false); if (e.data?.data?.error) { useOpenClawStore.getState().setSkillUpdateError(e.data.data.error); } else { useOpenClawStore.getState().setSkillUpdate(e.data?.data || null); useOpenClawStore.getState().setSkillUpdateError(null); } }, }; try { const handler = handlers[evt.type]; if (handler) handler(evt); else console.log('[handleEvent] Unknown event type:', evt.type); } catch (error) { console.error('[handleEvent] Error handling event:', evt.type, error); } }; // Create and connect WebSocket client const client = new ReadOnlyClient(handlePushEvent); clientRef.current = client; client.connect(); setConnectionStatus('connecting'); // Sync refs with store state isWatchlistSavingRef.current = isWatchlistSaving; isRuntimeConfigSavingRef.current = isRuntimeConfigSaving; selectedSkillAgentIdRef.current = selectedSkillAgentId; return () => { if (clientRef.current) { clientRef.current.disconnect(); } }; }, [ addSystemMessage, processFeedEvent, processHistoricalFeed, setAgentProfilesByAgent, setAgentSkillsByAgent, setAgentSkillsFeedback, setAgentSkillsSavingKey, setBubbles, setConnectionStatus, setCurrentDate, setDataSources, setExplainEventsByTicker, setHistorySourceByTicker, setHoldings, setInsiderTradesByTicker, setIsAgentSkillsLoading, setIsConnected, setIsRuntimeConfigSaving, setIsWatchlistSaving, setIsWorkspaceFileLoading, setLastDayHistory, setLeaderboard, setLocalSkillDraftsByKey, setMarketStatus, setNewsByTicker, setOhlcHistoryByTicker, setPortfolioData, setPriceHistoryByTicker, setProgress, setRollingTickers, setRuntimeConfig, setRuntimeConfigFeedback, setServerMode, setSkillDetailLoadingKey, setSkillDetailsByName, setStats, setSystemStatus, setTechnicalIndicatorsByTicker, setTickers, setTrades, setVirtualTime, setWatchlistFeedback, setWatchlistInputValue, setWorkspaceFileFeedback, setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, updateTickersFromPrices ]); // Sync refs useEffect(() => { isWatchlistSavingRef.current = isWatchlistSaving; }, [isWatchlistSaving]); useEffect(() => { isRuntimeConfigSavingRef.current = isRuntimeConfigSaving; }, [isRuntimeConfigSaving]); useEffect(() => { selectedSkillAgentIdRef.current = selectedSkillAgentId; }, [selectedSkillAgentId]); return { clientRef, setRequestStockHistory, setRequestStockNewsTimeline, setRequestStockNewsCategories }; }