1493 lines
59 KiB
JavaScript
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 };
|
|
}
|