Skip to content

Commit 68ff8ea

Browse files
committed
fix(review-feedback-1004): address latest review comments
1 parent 06c54d9 commit 68ff8ea

5 files changed

Lines changed: 134 additions & 4 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
1515
- [修复] 移除 `HistoryItem``ReportSummary` 响应 Schema 中 `sentiment_score``ge=0/le=100` 约束(fixes #942)——历史库中存储的超范围负值或大于 100 的情绪评分不再触发 Pydantic ValidationError,历史列表与详情接口恢复正常返回。
1616
- [改进] Agent IntelAgent 新增公司公告搜索维度(上交所/深交所/cninfo)与主力资金流工具(get_capital_flow),修复 Agent 模式下公告和资金流数据经常缺失的问题
1717
- [修复] webui_frontend.py 在 static/index.html 存在但 static/assets/ 缺失时发出明确警告,避免用户因 CSS/JS 资源缺失导致页面元素异常变大却无从排查
18+
- [修复] `StockAnalysisPipeline` 搜索服务与社交舆情服务改为可选降级初始化:任一服务初始化异常时记录 warning 并以禁用状态继续运行,避免外部依赖抖动阻塞主分析链路与 SSE 进度回调。
1819
- [文档] DEPLOY.md 和 deploy-webui-cloud.md 新增"UI 元素异常变大/布局错乱"排查步骤(重建 Docker 镜像或手动执行 npm run build)
1920
- [文档] 补充飞书 Webhook 配置说明:强调 `FEISHU_WEBHOOK_URL` 是群通知必填项、`FEISHU_WEBHOOK_SECRET` 与飞书机器人「签名校验」必须两端同时启用或同时关闭、`FEISHU_APP_SECRET` 仅用于应用/Stream Bot 模式不可替代 Webhook;同步完善英文指南并在 `.env.example` 为相关配置项补充内联说明注释
2021

docs/full-guide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,16 @@ daily_stock_analysis/
262262
| `BOCHA_API_KEYS` | 博查搜索 API Key(中文优化) | 可选 |
263263
| `BRAVE_API_KEYS` | Brave Search API Key(美股优化) | 可选 |
264264
| `SERPAPI_API_KEYS` | SerpAPI 备用搜索 | 可选 |
265+
| `SOCIAL_SENTIMENT_API_KEY` | Stock Sentiment API Key(Reddit / X / Polymarket,可选) | 可选 |
266+
| `SOCIAL_SENTIMENT_API_URL` | Stock Sentiment API 地址(默认 `https://api.adanos.org`| 可选 |
265267
| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时默认自动发现公共实例 | 可选 |
266268
| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `true`| 可选 |
267269
| `NEWS_STRATEGY_PROFILE` | 新闻策略窗口档位:`ultra_short`(1天)/`short`(3天)/`medium`(7天)/`long`(30天);实际窗口取与 `NEWS_MAX_AGE_DAYS` 的最小值 | 默认 `short` |
268270
| `NEWS_MAX_AGE_DAYS` | 新闻最大时效(天),搜索时限制结果在近期内 | 默认 `3` |
269271
| `BIAS_THRESHOLD` | 乖离率阈值(%),超过提示不追高;强势趋势股自动放宽到 1.5 倍 | 默认 `5.0` |
270272

273+
> 行为说明:搜索服务与社交舆情服务为可选增强链路。任一服务初始化失败时,系统会记录 warning 并降级为跳过该服务,仅影响对应环节,不会阻塞技术面主链路和主任务流。
274+
271275
### 数据源配置
272276

273277
| 变量名 | 说明 | 默认值 | 必填 |
@@ -982,6 +986,7 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
982986
> 说明:`POST /api/v1/analysis/analyze` 在 `async_mode=false` 时仅支持单只股票;批量 `stock_codes` 需使用 `async_mode=true`。异步 `202` 响应对单股返回 `task_id`,对批量返回 `accepted` / `duplicates` 汇总结构。
983987

984988
> 进度流说明:`GET /api/v1/analysis/tasks/stream` 除 `task_created / task_started / task_completed / task_failed` 外,新增 `task_progress` 事件。普通分析链路会在“行情准备 / 新闻检索 / 上下文整理 / LLM 生成 / 报告保存”等阶段持续更新 `progress` 与 `message`。LiteLLM 流式返回仅在服务端累积完整文本,最终 JSON 解析成功后才会持久化历史报告;若流式在首个 chunk 前不可用,会自动回退到原非流式调用;若已产生部分 chunk 后失败,系统先尝试同模型非流式重试,失败后再按既有主模型->备用模型顺序继续尝试。
989+
> 如果任务进度回调异常,主链路不会中断,系统会提升告警为 warning 级别并在服务端日志中输出完整异常,便于排查 SSE 推送断点。
985990
>
986991
> 说明:该特性属于运行时 SSE 与回退链路细节,优先记录于完整指南(`full-guide*.md`),不在 `README.md` 中展开详细行为分支。
987992

docs/full-guide_EN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,13 @@ Default schedule: Every weekday at **18:00 (Beijing Time)** automatic execution.
238238
| `BOCHA_API_KEYS` | Bocha Search API Key (Chinese optimized) | Optional |
239239
| `BRAVE_API_KEYS` | Brave Search API Key (US stocks optimized) | Optional |
240240
| `SERPAPI_API_KEYS` | SerpAPI Backup search | Optional |
241+
| `SOCIAL_SENTIMENT_API_KEY` | Stock Sentiment API Key (Reddit / X / Polymarket, US stocks optional) | Optional |
242+
| `SOCIAL_SENTIMENT_API_URL` | Stock Sentiment API endpoint (default `https://api.adanos.org`) | Optional |
241243
| `SEARXNG_BASE_URLS` | SearXNG self-hosted instances (quota-free fallback, enable format: json in settings.yml); when empty the app auto-discovers public instances | Optional |
242244
| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | Auto-discover public SearXNG instances from `searx.space` when `SEARXNG_BASE_URLS` is empty (default `true`) | Optional |
243245

246+
> Behavior note: Search and social sentiment are optional enhancement services. If either service fails to initialize, the system logs a warning and degrades gracefully by skipping that stage without blocking the core analysis flow.
247+
244248
### Data Source Configuration
245249

246250
| Variable | Description | Default | Required |
@@ -816,6 +820,7 @@ FastAPI provides RESTful API service for configuration management and triggering
816820
> Note: `POST /api/v1/analysis/analyze` supports only one stock when `async_mode=false`; batch `stock_codes` requires `async_mode=true`. The async `202` response returns a single `task_id` for one stock, or an `accepted` / `duplicates` summary for batch requests.
817821
818822
> Progress-stream note: `GET /api/v1/analysis/tasks/stream` now emits `task_progress` in addition to `task_created / task_started / task_completed / task_failed`. The regular analysis path updates `progress` and `message` across quote preparation, news retrieval, context assembly, LLM generation, and report persistence. Streaming chunks are accumulated only on the server side; history is persisted only after the final JSON parses successfully. If streaming is unavailable before the first chunk, the system falls back to the previous non-stream request. If a stream fails after partial output has already arrived, the system first retries non-stream for the same model, then continues through existing fallback models in the original order (primary + fallback list).
823+
> If a progress callback fails, the analysis flow continues, and the exception is now logged at warning level to help troubleshoot SSE delivery gaps.
819824
820825
> Note: This behavior is documented in the full guide (`full-guide*.md`) because it is detailed runtime SSE/fallback behavior and is therefore kept out of the README.
821826

src/core/pipeline.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __init__(
116116
news_strategy_profile=getattr(self.config, "news_strategy_profile", "short"),
117117
)
118118
except Exception as exc:
119-
logger.warning("搜索服务初始化失败,将以无搜索模式运行: %s", exc)
119+
logger.warning("搜索服务初始化失败,将以无搜索模式运行: %s", exc, exc_info=True)
120120
self.search_service = None
121121

122122
logger.info(f"调度器初始化完成,最大并发数: {self.max_workers}")
@@ -130,7 +130,9 @@ def __init__(
130130
logger.info("筹码分布分析已启用")
131131
else:
132132
logger.info("筹码分布分析已禁用")
133-
if self.search_service is not None and self.search_service.is_available:
133+
if self.search_service is None:
134+
logger.warning("搜索服务未启用(初始化失败或依赖缺失)")
135+
elif self.search_service.is_available:
134136
logger.info("搜索服务已启用")
135137
else:
136138
logger.warning("搜索服务未启用(未配置搜索能力)")
@@ -144,7 +146,11 @@ def __init__(
144146
if self.social_sentiment_service.is_available:
145147
logger.info("Social sentiment service enabled (Reddit/X/Polymarket, US stocks only)")
146148
except Exception as exc:
147-
logger.warning("社交舆情服务初始化失败,将跳过舆情分析: %s", exc)
149+
logger.warning(
150+
"社交舆情服务初始化失败,将跳过舆情分析: %s",
151+
exc,
152+
exc_info=True,
153+
)
148154
self.social_sentiment_service = None
149155

150156
def _emit_progress(self, progress: int, message: str) -> None:
@@ -155,7 +161,19 @@ def _emit_progress(self, progress: int, message: str) -> None:
155161
try:
156162
callback(progress, message)
157163
except Exception as exc:
158-
logger.warning("[pipeline] progress callback failed: %s", exc)
164+
query_id = getattr(self, "query_id", None)
165+
logger.warning(
166+
"[pipeline] progress callback failed: %s (progress=%s, message=%r, query_id=%s)",
167+
exc,
168+
progress,
169+
message,
170+
query_id,
171+
extra={
172+
"progress": progress,
173+
"progress_message": message,
174+
"query_id": query_id,
175+
},
176+
)
159177

160178
def fetch_and_save_stock_data(
161179
self,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# -*- coding: utf-8 -*-
2+
"""Regression tests for optional pipeline service degradation logs."""
3+
4+
import logging
5+
from types import SimpleNamespace
6+
from unittest.mock import MagicMock, patch
7+
8+
from src.core.pipeline import StockAnalysisPipeline
9+
10+
11+
def _make_config() -> SimpleNamespace:
12+
return SimpleNamespace(
13+
max_workers=2,
14+
save_context_snapshot=False,
15+
bocha_api_keys=[],
16+
tavily_api_keys=[],
17+
brave_api_keys=[],
18+
serpapi_keys=[],
19+
minimax_api_keys=[],
20+
searxng_base_urls=[],
21+
searxng_public_instances_enabled=False,
22+
news_max_age_days=7,
23+
news_strategy_profile="short",
24+
enable_realtime_quote=False,
25+
realtime_source_priority=[],
26+
enable_chip_distribution=False,
27+
social_sentiment_api_key="",
28+
social_sentiment_api_url="https://example.invalid/social",
29+
)
30+
31+
32+
def _build_pipeline(config: SimpleNamespace) -> StockAnalysisPipeline:
33+
with patch("src.core.pipeline.get_db", return_value=MagicMock()), \
34+
patch("src.core.pipeline.DataFetcherManager", return_value=MagicMock()), \
35+
patch("src.core.pipeline.StockTrendAnalyzer", return_value=MagicMock()), \
36+
patch("src.core.pipeline.GeminiAnalyzer", return_value=MagicMock()), \
37+
patch("src.core.pipeline.NotificationService", return_value=MagicMock()):
38+
return StockAnalysisPipeline(config=config)
39+
40+
41+
def test_search_service_init_failure_logs_traceback_and_failure_state(caplog):
42+
config = _make_config()
43+
social_service = MagicMock()
44+
social_service.is_available = False
45+
46+
with patch("src.core.pipeline.SearchService", side_effect=RuntimeError("search init boom")), \
47+
patch("src.core.pipeline.SocialSentimentService", return_value=social_service), \
48+
caplog.at_level(logging.WARNING, logger="src.core.pipeline"):
49+
pipeline = _build_pipeline(config)
50+
51+
assert pipeline.search_service is None
52+
53+
init_failure_records = [
54+
record for record in caplog.records if "搜索服务初始化失败,将以无搜索模式运行" in record.message
55+
]
56+
assert len(init_failure_records) == 1
57+
assert init_failure_records[0].exc_info is not None
58+
assert "搜索服务未启用(初始化失败或依赖缺失)" in caplog.text
59+
assert "搜索服务未启用(未配置搜索能力)" not in caplog.text
60+
61+
62+
def test_social_sentiment_init_failure_logs_traceback(caplog):
63+
config = _make_config()
64+
search_service = MagicMock()
65+
search_service.is_available = False
66+
67+
with patch("src.core.pipeline.SearchService", return_value=search_service), \
68+
patch("src.core.pipeline.SocialSentimentService", side_effect=RuntimeError("social init boom")), \
69+
caplog.at_level(logging.WARNING, logger="src.core.pipeline"):
70+
pipeline = _build_pipeline(config)
71+
72+
assert pipeline.social_sentiment_service is None
73+
74+
init_failure_records = [
75+
record for record in caplog.records if "社交舆情服务初始化失败,将跳过舆情分析" in record.message
76+
]
77+
assert len(init_failure_records) == 1
78+
assert init_failure_records[0].exc_info is not None
79+
80+
81+
def test_emit_progress_logs_context_when_callback_fails(caplog):
82+
pipeline = StockAnalysisPipeline.__new__(StockAnalysisPipeline)
83+
pipeline.query_id = "query-123"
84+
85+
def _fail_callback(progress, message):
86+
raise RuntimeError(f"cannot send {progress}:{message}")
87+
88+
pipeline.progress_callback = _fail_callback
89+
90+
with caplog.at_level(logging.WARNING, logger="src.core.pipeline"):
91+
pipeline._emit_progress(55, "fetching news")
92+
93+
records = [record for record in caplog.records if "progress callback failed" in record.message]
94+
assert len(records) == 1
95+
record = records[0]
96+
assert "progress=55" in record.message
97+
assert "message='fetching news'" in record.message
98+
assert "query_id=query-123" in record.message
99+
assert record.progress == 55
100+
assert record.progress_message == "fetching news"
101+
assert record.query_id == "query-123"

0 commit comments

Comments
 (0)