feat: 微服务架构拆分和前后端优化
后端: - 拆分出 agent_service, runtime_service, trading_service, news_service - Gateway 模块化拆分 (gateway_*.py) - 添加 domains/ 领域层 - 新增 control_client, runtime_client - 更新 start-dev.sh 支持 split 服务模式 前端: - 完善 API 服务层 (newsApi, tradingApi) - 更新 vite.config.js - Explain 组件优化 测试: - 添加多个服务 app 测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,19 @@ import { AGENTS, INITIAL_TICKERS } from './config/constants';
|
||||
// Services
|
||||
import { ReadOnlyClient } from './services/websocket';
|
||||
import { startRuntime, uploadAgentSkillZip } from './services/runtimeApi';
|
||||
import {
|
||||
fetchNewsCategoriesDirect,
|
||||
fetchNewsForDateDirect,
|
||||
fetchRangeExplainDirect,
|
||||
fetchSimilarDaysDirect,
|
||||
fetchStockStoryDirect,
|
||||
hasDirectNewsService
|
||||
} from './services/newsApi';
|
||||
import {
|
||||
fetchInsiderTradesDirect,
|
||||
fetchStockHistoryDirect,
|
||||
hasDirectTradingService
|
||||
} from './services/tradingApi';
|
||||
|
||||
// Hooks
|
||||
import { useFeedProcessor } from './hooks/useFeedProcessor';
|
||||
@@ -937,7 +950,7 @@ export default function LiveTradingApp() {
|
||||
|
||||
const requestStockHistory = useCallback((symbol, { force = false } = {}) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -945,6 +958,65 @@ export default function LiveTradingApp() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const endDate = currentDate
|
||||
? String(currentDate).slice(0, 10)
|
||||
: new Date().toISOString().slice(0, 10);
|
||||
const end = new Date(`${endDate}T00:00:00`);
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 120);
|
||||
const startDate = start.toISOString().slice(0, 10);
|
||||
|
||||
if (hasDirectTradingService()) {
|
||||
void fetchStockHistoryDirect(normalized, startDate, endDate)
|
||||
.then((payload) => {
|
||||
const prices = Array.isArray(payload?.prices) ? payload.prices : [];
|
||||
setOhlcHistoryByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: prices
|
||||
}));
|
||||
setPriceHistoryByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: prices
|
||||
.map((point) => {
|
||||
const price = Number(point?.close);
|
||||
const timestamp = point?.time;
|
||||
if (!timestamp || !Number.isFinite(price)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
timestamp: String(timestamp),
|
||||
label: String(timestamp),
|
||||
price
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
}));
|
||||
setHistorySourceByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: 'trading_service'
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct stock-history fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
const success = clientRef.current.send({
|
||||
type: 'get_stock_history',
|
||||
ticker: normalized,
|
||||
lookback_days: 120
|
||||
});
|
||||
if (success) {
|
||||
requestedStockHistoryRef.current.add(normalized);
|
||||
}
|
||||
}
|
||||
});
|
||||
requestedStockHistoryRef.current.add(normalized);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = clientRef.current.send({
|
||||
type: 'get_stock_history',
|
||||
ticker: normalized,
|
||||
@@ -956,7 +1028,7 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
|
||||
return success;
|
||||
}, []);
|
||||
}, [currentDate]);
|
||||
|
||||
const requestStockExplainEvents = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
@@ -984,9 +1056,49 @@ export default function LiveTradingApp() {
|
||||
|
||||
const requestStockNewsForDate = useCallback((symbol, date) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !date || !clientRef.current) {
|
||||
if (!normalized || !date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDirectNewsService()) {
|
||||
void fetchNewsForDateDirect(normalized, date, 20)
|
||||
.then((payload) => {
|
||||
const targetDate = typeof payload?.date === 'string' ? payload.date.trim() : date;
|
||||
const news = Array.isArray(payload?.news) ? payload.news : [];
|
||||
const freshness = payload?.freshness || null;
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
...(prev[normalized] || {}),
|
||||
byDate: {
|
||||
...((prev[normalized] && prev[normalized].byDate) || {}),
|
||||
[targetDate]: news
|
||||
},
|
||||
byDateFreshness: {
|
||||
...((prev[normalized] && prev[normalized].byDateFreshness) || {}),
|
||||
[targetDate]: freshness
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct news-for-date fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
clientRef.current.send({
|
||||
type: 'get_stock_news_for_date',
|
||||
ticker: normalized,
|
||||
date,
|
||||
limit: 20
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_news_for_date',
|
||||
ticker: normalized,
|
||||
@@ -1009,21 +1121,96 @@ export default function LiveTradingApp() {
|
||||
|
||||
const requestStockNewsCategories = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const endDate = currentDate
|
||||
? String(currentDate).slice(0, 10)
|
||||
: new Date().toISOString().slice(0, 10);
|
||||
const end = new Date(`${endDate}T00:00:00`);
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 90);
|
||||
const startDate = start.toISOString().slice(0, 10);
|
||||
|
||||
if (hasDirectNewsService()) {
|
||||
void fetchNewsCategoriesDirect(normalized, startDate, endDate, 200)
|
||||
.then((payload) => {
|
||||
const freshness = payload?.freshness || null;
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
...(prev[normalized] || {}),
|
||||
categories: payload?.categories || {},
|
||||
categoriesStartDate: startDate,
|
||||
categoriesEndDate: endDate,
|
||||
categoriesFreshness: freshness
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct news-categories fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
clientRef.current.send({
|
||||
type: 'get_stock_news_categories',
|
||||
ticker: normalized,
|
||||
lookback_days: 90
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_news_categories',
|
||||
ticker: normalized,
|
||||
lookback_days: 90
|
||||
});
|
||||
}, []);
|
||||
}, [currentDate]);
|
||||
|
||||
const requestStockInsiderTrades = useCallback((symbol, startDate = null, endDate = null) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDirectTradingService()) {
|
||||
void fetchInsiderTradesDirect(normalized, startDate, endDate, 50)
|
||||
.then((payload) => {
|
||||
const rows = Array.isArray(payload?.insider_trades) ? payload.insider_trades : [];
|
||||
setInsiderTradesByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
ticker: normalized,
|
||||
startDate: startDate || null,
|
||||
endDate: endDate || null,
|
||||
trades: rows
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct insider-trades fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
clientRef.current.send({
|
||||
type: 'get_stock_insider_trades',
|
||||
ticker: normalized,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
limit: 50
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_insider_trades',
|
||||
ticker: normalized,
|
||||
@@ -1046,9 +1233,52 @@ export default function LiveTradingApp() {
|
||||
|
||||
const requestStockRangeExplain = useCallback((symbol, startDate, endDate, articleIds = []) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !startDate || !endDate || !clientRef.current) {
|
||||
if (!normalized || !startDate || !endDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDirectNewsService()) {
|
||||
void fetchRangeExplainDirect(normalized, startDate, endDate, articleIds)
|
||||
.then((payload) => {
|
||||
const result = payload?.result && typeof payload.result === 'object' ? payload.result : null;
|
||||
const freshness = payload?.freshness || null;
|
||||
if (!result?.start_date || !result?.end_date) {
|
||||
return;
|
||||
}
|
||||
const cacheKey = `${result.start_date}:${result.end_date}`;
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
...(prev[normalized] || {}),
|
||||
rangeExplainCache: {
|
||||
...((prev[normalized] && prev[normalized].rangeExplainCache) || {}),
|
||||
[cacheKey]: {
|
||||
...result,
|
||||
freshness
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct range explain fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
clientRef.current.send({
|
||||
type: 'get_stock_range_explain',
|
||||
ticker: normalized,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
article_ids: Array.isArray(articleIds) ? articleIds : []
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_range_explain',
|
||||
ticker: normalized,
|
||||
@@ -1060,9 +1290,51 @@ export default function LiveTradingApp() {
|
||||
|
||||
const requestStockStory = useCallback((symbol, asOfDate = null) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDirectNewsService()) {
|
||||
void fetchStockStoryDirect(normalized, asOfDate)
|
||||
.then((payload) => {
|
||||
const storyDate = typeof payload?.as_of_date === 'string' ? payload.as_of_date.trim() : '';
|
||||
const freshness = payload?.freshness || null;
|
||||
if (!storyDate) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
...(prev[normalized] || {}),
|
||||
storyCache: {
|
||||
...((prev[normalized] && prev[normalized].storyCache) || {}),
|
||||
[storyDate]: {
|
||||
story: payload.story || '',
|
||||
source: payload.source || 'news_service',
|
||||
asOfDate: storyDate,
|
||||
freshness
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct story fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
clientRef.current.send({
|
||||
type: 'get_stock_story',
|
||||
ticker: normalized,
|
||||
as_of_date: asOfDate
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_story',
|
||||
ticker: normalized,
|
||||
@@ -1072,9 +1344,46 @@ export default function LiveTradingApp() {
|
||||
|
||||
const requestStockSimilarDays = useCallback((symbol, date, topK = 8) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !date || !clientRef.current) {
|
||||
if (!normalized || !date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDirectNewsService()) {
|
||||
void fetchSimilarDaysDirect(normalized, date, topK)
|
||||
.then((payload) => {
|
||||
const targetDate = typeof payload?.target_date === 'string' ? payload.target_date.trim() : date;
|
||||
if (!targetDate) {
|
||||
return;
|
||||
}
|
||||
setNewsByTicker((prev) => ({
|
||||
...prev,
|
||||
[normalized]: {
|
||||
...(prev[normalized] || {}),
|
||||
similarDaysCache: {
|
||||
...((prev[normalized] && prev[normalized].similarDaysCache) || {}),
|
||||
[targetDate]: payload
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Direct similar-days fetch failed, falling back to websocket:', error);
|
||||
if (clientRef.current) {
|
||||
clientRef.current.send({
|
||||
type: 'get_stock_similar_days',
|
||||
ticker: normalized,
|
||||
date,
|
||||
top_k: topK
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_similar_days',
|
||||
ticker: normalized,
|
||||
@@ -1707,7 +2016,8 @@ export default function LiveTradingApp() {
|
||||
items: Array.isArray(e.news) ? e.news : [],
|
||||
source: e.source || null,
|
||||
startDate: e.start_date || null,
|
||||
endDate: e.end_date || null
|
||||
endDate: e.end_date || null,
|
||||
freshness: e.freshness || null
|
||||
}
|
||||
}));
|
||||
requestStockNewsTimeline(symbol);
|
||||
@@ -1726,6 +2036,10 @@ export default function LiveTradingApp() {
|
||||
byDate: {
|
||||
...((prev[symbol] && prev[symbol].byDate) || {}),
|
||||
[date]: Array.isArray(e.news) ? e.news : []
|
||||
},
|
||||
byDateFreshness: {
|
||||
...((prev[symbol] && prev[symbol].byDateFreshness) || {}),
|
||||
[date]: e.freshness || null
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -1742,7 +2056,8 @@ export default function LiveTradingApp() {
|
||||
...(prev[symbol] || {}),
|
||||
timeline: Array.isArray(e.timeline) ? e.timeline : [],
|
||||
timelineStartDate: e.start_date || null,
|
||||
timelineEndDate: e.end_date || null
|
||||
timelineEndDate: e.end_date || null,
|
||||
timelineFreshness: e.freshness || null
|
||||
}
|
||||
}));
|
||||
},
|
||||
@@ -1758,7 +2073,8 @@ export default function LiveTradingApp() {
|
||||
...(prev[symbol] || {}),
|
||||
categories: e.categories || {},
|
||||
categoriesStartDate: e.start_date || null,
|
||||
categoriesEndDate: e.end_date || null
|
||||
categoriesEndDate: e.end_date || null,
|
||||
categoriesFreshness: e.freshness || null
|
||||
}
|
||||
}));
|
||||
},
|
||||
@@ -1805,7 +2121,10 @@ export default function LiveTradingApp() {
|
||||
...(prev[symbol] || {}),
|
||||
rangeExplainCache: {
|
||||
...((prev[symbol] && prev[symbol].rangeExplainCache) || {}),
|
||||
[cacheKey]: result
|
||||
[cacheKey]: {
|
||||
...result,
|
||||
freshness: e.freshness || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -1826,7 +2145,8 @@ export default function LiveTradingApp() {
|
||||
[asOfDate]: {
|
||||
story: e.story || '',
|
||||
source: e.source || null,
|
||||
asOfDate
|
||||
asOfDate,
|
||||
freshness: e.freshness || null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1852,7 +2172,8 @@ export default function LiveTradingApp() {
|
||||
[date]: {
|
||||
target_features: e.target_features || {},
|
||||
items: Array.isArray(e.items) ? e.items : [],
|
||||
error: e.error || null
|
||||
error: e.error || null,
|
||||
freshness: e.freshness || null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ export default function StockExplainView({
|
||||
visibleNews,
|
||||
newsCategories,
|
||||
visibleNewsByCategory,
|
||||
selectedNewsFreshness,
|
||||
selectedRangeWindow,
|
||||
selectedRangeExplain,
|
||||
latestSignal,
|
||||
@@ -337,6 +338,7 @@ export default function StockExplainView({
|
||||
newsSnapshot={newsSnapshot}
|
||||
visibleNewsByCategory={visibleNewsByCategory}
|
||||
visibleNews={visibleNews}
|
||||
selectedNewsFreshness={selectedNewsFreshness}
|
||||
activeNewsCategory={activeNewsCategory}
|
||||
onSelectNewsCategory={setActiveNewsCategory}
|
||||
activeNewsSentiment={activeNewsSentiment}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React from 'react';
|
||||
import { formatDateTime } from '../../utils/formatters';
|
||||
|
||||
function renderFreshness(freshness) {
|
||||
if (!freshness || typeof freshness !== 'object') return null;
|
||||
const lastFetch = freshness.last_news_fetch || '-';
|
||||
return `新闻更新到 ${lastFetch}${freshness.refreshed ? ' · 本次已刷新' : ''}`;
|
||||
}
|
||||
|
||||
function categoryLabel(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
const labels = {
|
||||
@@ -47,6 +53,7 @@ export default function ExplainNewsSection({
|
||||
newsSnapshot,
|
||||
visibleNewsByCategory,
|
||||
visibleNews,
|
||||
selectedNewsFreshness,
|
||||
activeNewsCategory,
|
||||
onSelectNewsCategory,
|
||||
activeNewsSentiment,
|
||||
@@ -64,6 +71,11 @@ export default function ExplainNewsSection({
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{newsSnapshot?.source ? `最近 ${visibleNewsByCategory.length} 条 · ${newsSnapshot.source}` : `最近 ${visibleNewsByCategory.length} 条真实新闻`}
|
||||
</div>
|
||||
{renderFreshness(selectedNewsFreshness) ? (
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{renderFreshness(selectedNewsFreshness)}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React from 'react';
|
||||
import { formatTickerPrice } from '../../utils/formatters';
|
||||
|
||||
function renderFreshness(freshness) {
|
||||
if (!freshness || typeof freshness !== 'object') return null;
|
||||
const lastFetch = freshness.last_news_fetch || '-';
|
||||
return `新闻更新到 ${lastFetch}${freshness.refreshed ? ' · 本次已刷新' : ''}`;
|
||||
}
|
||||
|
||||
function renderSentimentLabel(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'positive') return '利多';
|
||||
@@ -94,6 +100,11 @@ export default function ExplainRangeSection({
|
||||
: `分析来源 · ${renderAnalysisSourceLabel(selectedRangeExplain.analysis.analysis_source)}`}
|
||||
</div>
|
||||
) : null}
|
||||
{renderFreshness(selectedRangeExplain?.freshness) ? (
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{renderFreshness(selectedRangeExplain?.freshness)}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
function renderFreshness(freshness) {
|
||||
if (!freshness || typeof freshness !== 'object') return null;
|
||||
const lastFetch = freshness.last_news_fetch || '-';
|
||||
return `新闻更新到 ${lastFetch}${freshness.refreshed ? ' · 本次已刷新' : ''}`;
|
||||
}
|
||||
|
||||
export default function ExplainSimilarDaysSection({
|
||||
selectedSimilarDays,
|
||||
selectedEventDate,
|
||||
@@ -15,6 +21,11 @@ export default function ExplainSimilarDaysSection({
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{selectedEventDate || '先选择一个事件日期'}
|
||||
</div>
|
||||
{renderFreshness(selectedSimilarDays?.freshness) ? (
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{renderFreshness(selectedSimilarDays?.freshness)}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
|
||||
@@ -2,6 +2,12 @@ import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
function renderFreshness(freshness) {
|
||||
if (!freshness || typeof freshness !== 'object') return null;
|
||||
const lastFetch = freshness.last_news_fetch || '-';
|
||||
return `新闻更新到 ${lastFetch}${freshness.refreshed ? ' · 本次已刷新' : ''}`;
|
||||
}
|
||||
|
||||
export default function ExplainStorySection({
|
||||
selectedStory,
|
||||
selectedSymbol,
|
||||
@@ -17,6 +23,11 @@ export default function ExplainStorySection({
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{selectedStory?.asOfDate || currentDate || '按当前解释窗口生成'}
|
||||
</div>
|
||||
{renderFreshness(selectedStory?.freshness) ? (
|
||||
<div style={{ fontSize: 11, color: '#666666' }}>
|
||||
{renderFreshness(selectedStory?.freshness)}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
|
||||
@@ -226,6 +226,13 @@ export default function useExplainModel({
|
||||
return similarCache[selectedEventDate] || null;
|
||||
}, [newsSnapshot, selectedEventDate]);
|
||||
|
||||
const selectedNewsFreshness = useMemo(() => {
|
||||
if (selectedEventDate && newsSnapshot?.byDateFreshness?.[selectedEventDate]) {
|
||||
return newsSnapshot.byDateFreshness[selectedEventDate];
|
||||
}
|
||||
return newsSnapshot?.categoriesFreshness || newsSnapshot?.timelineFreshness || newsSnapshot?.freshness || null;
|
||||
}, [newsSnapshot, selectedEventDate]);
|
||||
|
||||
const latestSignal = tickerSignals[0] || null;
|
||||
const priceColor = selectedTicker?.change > 0 ? '#00C853' : selectedTicker?.change < 0 ? '#FF1744' : '#000000';
|
||||
const exposureWeight = holding && Number.isFinite(Number(holding.weight)) ? Number(holding.weight) * 100 : null;
|
||||
@@ -644,6 +651,7 @@ export default function useExplainModel({
|
||||
visibleNews,
|
||||
newsCategories,
|
||||
visibleNewsByCategory,
|
||||
selectedNewsFreshness,
|
||||
selectedRangeWindow,
|
||||
selectedRangeExplain,
|
||||
selectedStory,
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
* Application Configuration Constants
|
||||
*/
|
||||
|
||||
const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
|
||||
const isLocalDevHost = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
const host = String(window.location.hostname || "").trim().toLowerCase();
|
||||
return host === "localhost" || host === "127.0.0.1";
|
||||
};
|
||||
|
||||
// Centralized CDN asset URLs
|
||||
export const CDN_ASSETS = {
|
||||
companyRoom: {
|
||||
@@ -130,7 +139,25 @@ export const CHART_MARGIN = { left: 60, right: 20, top: 20, bottom: 40 };
|
||||
export const AXIS_TICKS = 5;
|
||||
|
||||
// WebSocket configuration
|
||||
export const WS_URL = import.meta.env.VITE_WS_URL || "ws://localhost:8000";
|
||||
const DEFAULT_CONTROL_API_BASE = isLocalDevHost()
|
||||
? "http://localhost:8000/api"
|
||||
: "/api";
|
||||
const DEFAULT_RUNTIME_API_BASE = isLocalDevHost()
|
||||
? "http://localhost:8003/api/runtime"
|
||||
: `${DEFAULT_CONTROL_API_BASE}/runtime`;
|
||||
export const CONTROL_API_BASE =
|
||||
trimTrailingSlash(import.meta.env.VITE_CONTROL_API_BASE_URL || "") || DEFAULT_CONTROL_API_BASE;
|
||||
export const RUNTIME_API_BASE =
|
||||
trimTrailingSlash(import.meta.env.VITE_RUNTIME_API_BASE_URL || "") ||
|
||||
DEFAULT_RUNTIME_API_BASE;
|
||||
const FALLBACK_WS_PROTOCOL =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:"
|
||||
? "wss:"
|
||||
: "ws:";
|
||||
const FALLBACK_WS_HOST =
|
||||
typeof window !== "undefined" ? window.location.hostname : "localhost";
|
||||
export const WS_URL =
|
||||
import.meta.env.VITE_WS_URL || `${FALLBACK_WS_PROTOCOL}//${FALLBACK_WS_HOST}:8765`;
|
||||
|
||||
// Initial ticker symbols for the production watchlist
|
||||
export const INITIAL_TICKERS = [
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { startTransition } from 'react';
|
||||
import { CONTROL_API_BASE, RUNTIME_API_BASE } from '../config/constants';
|
||||
|
||||
const BASE_PATH = '/api';
|
||||
|
||||
async function safeFetch(endpoint) {
|
||||
const response = await fetch(`${BASE_PATH}${endpoint}`);
|
||||
async function safeFetch(basePath, endpoint) {
|
||||
const response = await fetch(`${basePath}${endpoint}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function safeRequest(endpoint, options = {}) {
|
||||
async function safeRequest(basePath, endpoint, options = {}) {
|
||||
const isFormData = options.body instanceof FormData;
|
||||
const response = await fetch(`${BASE_PATH}${endpoint}`, {
|
||||
const response = await fetch(`${basePath}${endpoint}`, {
|
||||
headers: isFormData
|
||||
? { ...(options.headers || {}) }
|
||||
: {
|
||||
@@ -28,23 +27,23 @@ async function safeRequest(endpoint, options = {}) {
|
||||
}
|
||||
|
||||
export function fetchRuntimeContext() {
|
||||
return safeFetch('/runtime/context');
|
||||
return safeFetch(RUNTIME_API_BASE, '/context');
|
||||
}
|
||||
|
||||
export function fetchRuntimeAgents() {
|
||||
return safeFetch('/runtime/agents');
|
||||
return safeFetch(RUNTIME_API_BASE, '/agents');
|
||||
}
|
||||
|
||||
export function fetchRuntimeEvents() {
|
||||
return safeFetch('/runtime/events');
|
||||
return safeFetch(RUNTIME_API_BASE, '/events');
|
||||
}
|
||||
|
||||
export function fetchPendingApprovals() {
|
||||
return safeFetch('/guard/pending');
|
||||
return safeFetch(CONTROL_API_BASE, '/guard/pending');
|
||||
}
|
||||
|
||||
export function approvePendingApproval(approvalId) {
|
||||
return safeRequest('/guard/approve', {
|
||||
return safeRequest(CONTROL_API_BASE, '/guard/approve', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
approval_id: approvalId,
|
||||
@@ -55,7 +54,7 @@ export function approvePendingApproval(approvalId) {
|
||||
}
|
||||
|
||||
export function denyPendingApproval(approvalId, reason = 'Rejected from runtime panel') {
|
||||
return safeRequest('/guard/deny', {
|
||||
return safeRequest(CONTROL_API_BASE, '/guard/deny', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
approval_id: approvalId,
|
||||
@@ -90,7 +89,7 @@ export function loadAllRuntimeState(onSuccess, onError) {
|
||||
* If a runtime is already running, it will be forcefully stopped first.
|
||||
*/
|
||||
export function startRuntime(config) {
|
||||
return safeRequest('/runtime/start', {
|
||||
return safeRequest(RUNTIME_API_BASE, '/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
@@ -100,7 +99,7 @@ export function startRuntime(config) {
|
||||
* Stop the current running runtime.
|
||||
*/
|
||||
export function stopRuntime(force = true) {
|
||||
return safeRequest(`/runtime/stop?force=${force}`, {
|
||||
return safeRequest(RUNTIME_API_BASE, `/stop?force=${force}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
@@ -109,7 +108,7 @@ export function stopRuntime(force = true) {
|
||||
* Restart the runtime with a new configuration.
|
||||
*/
|
||||
export function restartRuntime(config) {
|
||||
return safeRequest('/runtime/restart', {
|
||||
return safeRequest(RUNTIME_API_BASE, '/restart', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
@@ -119,7 +118,7 @@ export function restartRuntime(config) {
|
||||
* Get information about the currently running runtime.
|
||||
*/
|
||||
export function fetchCurrentRuntime() {
|
||||
return safeFetch('/runtime/current');
|
||||
return safeFetch(RUNTIME_API_BASE, '/current');
|
||||
}
|
||||
|
||||
export async function uploadAgentSkillZip({
|
||||
@@ -149,6 +148,7 @@ export async function uploadAgentSkillZip({
|
||||
}
|
||||
|
||||
return safeRequest(
|
||||
CONTROL_API_BASE,
|
||||
`/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/skills/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Fetches Gateway port from API before connecting
|
||||
*/
|
||||
|
||||
import { WS_URL } from "../config/constants";
|
||||
import { RUNTIME_API_BASE, WS_URL } from "../config/constants";
|
||||
|
||||
// Global port cache
|
||||
let cachedGatewayPort = null;
|
||||
@@ -15,7 +15,7 @@ let cachedWsUrl = null;
|
||||
*/
|
||||
export async function fetchGatewayPort() {
|
||||
try {
|
||||
const response = await fetch('/api/runtime/gateway/port');
|
||||
const response = await fetch(`${RUNTIME_API_BASE}/gateway/port`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user