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:
2026-03-23 17:45:39 +08:00
parent 0f1bc2bb39
commit 3448667b79
54 changed files with 5440 additions and 2947 deletions

View File

@@ -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}

View File

@@ -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={{

View File

@@ -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={{

View File

@@ -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={{

View File

@@ -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={{

View File

@@ -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,