Files
evotraders/frontend/src/hooks/useWebSocketConnection.js
2026-03-30 17:46:44 +08:00

1493 lines
59 KiB
JavaScript

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(/<final>([\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 /<final>[\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) => /<final>[\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 };
}