Skip to content

Commit c7f7c3c

Browse files
ZhuLinsenAnyone878
authored andcommitted
fix: 大盘分析的历史执行记录丢失 (ZhuLinsen#1306) (ZhuLinsen#1307)
* fix(issue-1306): 大盘分析的历史执行记录丢失 * fix(review-feedback-1307): also special-case the history detail/client path to surface the saved * fix(review-feedback-1307): 补齐大盘复盘历史记录的 Web/API 契约闭环与对应文档落点 * fix(review-feedback-1307): Disable stock rerun for market-review history rows * fix(review-feedback-1307): Disable follow-up for market-review history rows * fix(review-feedback-1307): 避免大盘复盘历史被整体回测当作普通股票分析记录处理 * fix(review-feedback-1307): 澄清结构化检测提示中的外部模型/API 与运行时配置迁移风险是否为误报,或补齐对应兼容性证据 * fix(review-feedback-1307): 修复普通个股历史不回归问题
1 parent 98edd51 commit c7f7c3c

15 files changed

Lines changed: 455 additions & 23 deletions

api/v1/endpoints/analysis.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import json
2121
import logging
2222
import re
23+
import uuid
2324
from datetime import datetime
2425
from pathlib import Path
2526
from typing import Optional, Union, Dict, Any
@@ -116,20 +117,24 @@ def _run_market_review_background(
116117
override_region: Optional[str] = None,
117118
lock_token: Optional[_MarketReviewExecutionLock] = None,
118119
config: Optional[Config] = None,
120+
query_id: Optional[str] = None,
119121
) -> None:
120122
"""Run market review after the API response has been accepted."""
121123
from src.core.market_review import run_market_review
122124

123125
runtime_config = config or get_config_dep()
124126
try:
125127
notifier, analyzer, search_service = _build_market_review_runtime(runtime_config)
126-
report = run_market_review(
127-
notifier=notifier,
128-
analyzer=analyzer,
129-
search_service=search_service,
130-
send_notification=send_notification,
131-
override_region=override_region,
132-
)
128+
review_kwargs = {
129+
"notifier": notifier,
130+
"analyzer": analyzer,
131+
"search_service": search_service,
132+
"send_notification": send_notification,
133+
"override_region": override_region,
134+
}
135+
if query_id:
136+
review_kwargs["query_id"] = query_id
137+
report = run_market_review(**review_kwargs)
133138
if not report:
134139
raise RuntimeError("大盘复盘未返回可持久化报告")
135140
return {"result": report}
@@ -500,16 +505,19 @@ def trigger_market_review(
500505
)
501506

502507
try:
508+
task_id = uuid.uuid4().hex
503509
task = get_task_queue().submit_background_task(
504510
lambda: _run_market_review_background(
505511
request.send_notification,
506512
override_region=override_region,
507513
lock_token=lock_token,
508514
config=config,
515+
query_id=task_id,
509516
),
510517
stock_code="market_review",
511518
stock_name="大盘复盘",
512519
message="大盘复盘任务已提交",
520+
task_id=task_id,
513521
)
514522
except Exception:
515523
_release_market_review_lock(lock_token)
@@ -751,6 +759,25 @@ def get_analysis_status(task_id: str) -> TaskStatus:
751759
if records:
752760
record = records[0]
753761
raw_result = parse_json_field(record.raw_result)
762+
if getattr(record, "report_type", None) == "market_review":
763+
market_review_report = None
764+
if isinstance(raw_result, dict):
765+
report_text = raw_result.get("raw_response") or raw_result.get("market_review_report")
766+
if isinstance(report_text, str) and report_text.strip():
767+
market_review_report = report_text
768+
if not market_review_report and record.news_content:
769+
market_review_report = record.news_content
770+
771+
return TaskStatus(
772+
task_id=task_id,
773+
status="completed",
774+
progress=100,
775+
result=None,
776+
market_review_report=market_review_report,
777+
error=None,
778+
stock_name=record.name,
779+
)
780+
754781
model_used = normalize_model_used(
755782
(raw_result or {}).get("model_used") if isinstance(raw_result, dict) else None
756783
)

apps/dsa-web/src/pages/HomePage.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const HomePage: React.FC = () => {
119119

120120
const reportLanguage = normalizeReportLanguage(selectedReport?.meta.reportLanguage);
121121
const reportText = getReportText(reportLanguage);
122+
const isMarketReviewHistoryReport = selectedReport?.meta.reportType === 'market_review';
122123
const setupNeedsAction = setupStatus ? !setupStatus.isComplete : false;
123124
const setupMissingLabels = useMemo(() => {
124125
if (!setupStatus) {
@@ -161,7 +162,7 @@ const HomePage: React.FC = () => {
161162
);
162163

163164
const handleAskFollowUp = useCallback(() => {
164-
if (selectedReport?.meta.id === undefined) {
165+
if (selectedReport?.meta.id === undefined || selectedReport.meta.reportType === 'market_review') {
165166
return;
166167
}
167168

@@ -172,7 +173,7 @@ const HomePage: React.FC = () => {
172173
}, [navigate, selectedReport]);
173174

174175
const handleReanalyze = useCallback(() => {
175-
if (!selectedReport) {
176+
if (!selectedReport || selectedReport.meta.reportType === 'market_review') {
176177
return;
177178
}
178179

@@ -584,7 +585,7 @@ const HomePage: React.FC = () => {
584585
<Button
585586
variant="home-action-ai"
586587
size="sm"
587-
disabled={isAnalyzing || selectedReport.meta.id === undefined}
588+
disabled={isAnalyzing || selectedReport.meta.id === undefined || isMarketReviewHistoryReport}
588589
onClick={handleReanalyze}
589590
>
590591
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -595,7 +596,7 @@ const HomePage: React.FC = () => {
595596
<Button
596597
variant="home-action-ai"
597598
size="sm"
598-
disabled={selectedReport.meta.id === undefined}
599+
disabled={selectedReport.meta.id === undefined || isMarketReviewHistoryReport}
599600
onClick={handleAskFollowUp}
600601
>
601602
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

apps/dsa-web/src/pages/__tests__/HomePage.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,33 @@ const historyReport = {
7878
},
7979
};
8080

81+
const marketReviewHistoryItem = {
82+
id: 2,
83+
queryId: 'market-review-q-1',
84+
stockCode: 'MARKET',
85+
stockName: '大盘复盘',
86+
reportType: 'market_review' as const,
87+
createdAt: '2026-03-18T08:00:00Z',
88+
};
89+
90+
const marketReviewHistoryReport = {
91+
meta: {
92+
id: 2,
93+
queryId: 'market-review-q-1',
94+
stockCode: 'MARKET',
95+
stockName: '大盘复盘',
96+
reportType: 'market_review' as const,
97+
reportLanguage: 'zh' as const,
98+
createdAt: '2026-03-18T08:00:00Z',
99+
},
100+
summary: {
101+
analysisSummary: '大盘复盘摘要',
102+
operationAdvice: '查看复盘',
103+
trendPrediction: '大盘复盘',
104+
sentimentScore: 50,
105+
},
106+
};
107+
81108
describe('HomePage', () => {
82109
beforeEach(() => {
83110
vi.clearAllMocks();
@@ -491,4 +518,33 @@ describe('HomePage', () => {
491518
forceRefresh: true,
492519
}));
493520
});
521+
522+
it('disables stock reanalysis and follow-up for market review history reports', async () => {
523+
vi.mocked(historyApi.getList).mockResolvedValue({
524+
total: 1,
525+
page: 1,
526+
limit: 20,
527+
items: [marketReviewHistoryItem],
528+
});
529+
vi.mocked(historyApi.getDetail).mockResolvedValue(marketReviewHistoryReport);
530+
531+
render(
532+
<MemoryRouter>
533+
<HomePage />
534+
</MemoryRouter>,
535+
);
536+
537+
await screen.findByText('大盘复盘摘要');
538+
const reanalyzeButton = screen.getByRole('button', { name: '重新分析' });
539+
const followUpButton = screen.getByRole('button', { name: '追问 AI' });
540+
541+
expect(reanalyzeButton).toBeDisabled();
542+
expect(followUpButton).toBeDisabled();
543+
544+
fireEvent.click(reanalyzeButton);
545+
fireEvent.click(followUpButton);
546+
547+
expect(analysisApi.analyzeAsync).not.toHaveBeenCalled();
548+
expect(navigateMock).not.toHaveBeenCalled();
549+
});
494550
});

apps/dsa-web/src/types/analysis.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
// ============ Request Types ============
77

8+
export type StockReportType = 'simple' | 'detailed' | 'full' | 'brief';
9+
export type ReportType = StockReportType | 'market_review';
10+
811
export interface AnalysisRequest {
912
stockCode?: string;
1013
stockCodes?: string[];
11-
reportType?: 'simple' | 'detailed' | 'full' | 'brief';
14+
reportType?: StockReportType;
1215
forceRefresh?: boolean;
1316
asyncMode?: boolean;
1417
stockName?: string;
@@ -38,7 +41,7 @@ export interface ReportMeta {
3841
queryId: string;
3942
stockCode: string;
4043
stockName: string;
41-
reportType: 'simple' | 'detailed' | 'full' | 'brief';
44+
reportType: ReportType;
4245
reportLanguage?: ReportLanguage;
4346
createdAt: string;
4447
currentPrice?: number;
@@ -206,7 +209,7 @@ export interface HistoryItem {
206209
queryId: string; // Linked analysis query ID
207210
stockCode: string;
208211
stockName?: string;
209-
reportType?: string;
212+
reportType?: ReportType;
210213
sentimentScore?: number;
211214
operationAdvice?: string;
212215
createdAt: string;

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
4747
- [修复] 调高基本面聚合默认超时预算,降低 Windows/Docker 环境下整段基本面 timeout 的概率。
4848
- [修复] 正式分析链路兼容 OpenAI-compatible `content_blocks` 响应,避免 `message.content=null` 时被误判为空回复。
4949
- [文档] Issue #1279 外部响应兼容补证据:本次修复以 `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0` 为运行时前提,交叉参照 [LiteLLM OpenAI-compatible](https://docs.litellm.ai/docs/providers/openai_compatible) / [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat)、并以 `tests/test_market_analyzer_generate_text.py``content_blocks``list content` 回归样例为复现依据,保留 `message.content` 回退逻辑避免兼容断层。
50+
- [文档] Issue #1306 明确本轮仅持久化大盘复盘历史,不改 LLM 模型名、provider、Base URL、LiteLLM 运行时清理逻辑;兼容性依据为本仓库 `requirements.txt` 锁定版本与现有 `docs/LLM_CONFIG_GUIDE*.md` 兼容说明,回退路径为回滚本版本,见 `tests/test_analysis_api_contract.py``tests/test_analysis_history.py``tests/test_market_review.py`
5051
- [改进] 大盘复盘新增 `MARKET_REVIEW_COLOR_SCHEME` 配置,可在指数涨跌幅中选择绿涨红跌或红涨绿跌。
5152
- [文档] 明确 `MARKET_REVIEW_COLOR_SCHEME` 仅为大盘复盘展示配置,枚举为 `green_up`/`red_up`(默认 `green_up`),属于文案与颜色语义层面变更;本次未调整模型名、provider、Base URL、LLM 运行时迁移或运行时清理逻辑。
53+
- [修复] 大盘复盘执行结果写入现有分析历史,Web 历史列表可直接查看已生成复盘,避免重复触发分析。
5254

5355
## [3.16.0] - 2026-05-10
5456

docs/full-guide.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ daily_stock_analysis/
208208
### AI 模型配置
209209

210210
> 完整说明见 [LLM 配置指南](LLM_CONFIG_GUIDE.md)(三层配置、渠道模式、Vision、Agent、排错);常用服务商预设、Actions 变量对照和错误排障见 [LLM 服务商配置指南](llm-providers.md)
211+
> 兼容性说明(Issue #1306):本次改动只复用已有历史写入链路展示大盘复盘结果,不修改模型名、provider、Base URL、`LiteLLM` 清理/兼容语义。回退路径为回滚本版本。兼容验证来源见 `requirements.txt``litellm` 版本约束)、`docs/LLM_CONFIG_GUIDE*.md`,以及回归用例 `tests/test_analysis_api_contract.py``tests/test_analysis_history.py``tests/test_market_review.py`;官方源参考:[LiteLLM OpenAI-compatible](https://docs.litellm.ai/docs/providers/openai_compatible)[OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat)
211212
> 本节仅同步模型/渠道配置清单,不额外引入新的外部 provider / Base URL 兼容约定;兼容语义以当前仓库 `requirements.txt` 依赖约束和相关测试为准,历史回退路径见上述两份文档中“回退/恢复”说明。
212213
213214
| 变量名 | 说明 | 默认值 | 必填 |
@@ -1151,6 +1152,7 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
11511152
- 🧭 **首次配置提示** - 首页会读取只读配置状态,缺少 LLM 主渠道、自选股等基础项时提示缺口并引导进入系统设置
11521153
- 📊 **实时进度** - 分析任务状态实时更新,支持多任务并行;普通分析链路在进入 LLM 阶段后会优先尝试 LiteLLM 流式生成,并通过任务 SSE 回灌更细粒度的 `message/progress`
11531154
- 🗂️ **大盘复盘任务可见性** - 首页触发大盘复盘后会返回 `task_id` 并轮询 `GET /api/v1/analysis/status/{task_id}`,在进行中/完成/失败场景给出可见反馈,失败时直接透出报错内容
1155+
- 🧾 **市场复盘历史可复用** - 大盘复盘任务会持久化到分析历史,`report_type` 为 `market_review`,可直接通过历史列表/详情打开对应 Markdown 或详情页,不会重新触发分析重算
11541156
- 📈 **回测验证** - 评估历史分析准确率,查询方向胜率与模拟收益
11551157
- 🔗 **API 文档** - 访问 `/docs` 查看 Swagger UI
11561158

@@ -1177,6 +1179,7 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
11771179
> 说明:`POST /api/v1/analysis/analyze` 在 `async_mode=false` 时仅支持单只股票;批量 `stock_codes` 需使用 `async_mode=true`。异步 `202` 响应对单股返回 `task_id`,对批量返回 `accepted` / `duplicates` 汇总结构。
11781180
> 说明:`POST /api/v1/analysis/market-review` 采用后端与 CLI/Bot 共用的配置路径(`GeminiAnalyzer(config=...)` 与同样的搜索/提示词构造入口)。Provider 兼容路由会优先识别并使用 `litellm_model`、`llm_model_list`,若未配置则回退 legacy `GEMINI_*`、`OPENAI_*`、`ANTHROPIC_*`、`DEEPSEEK_*` 键;不会新增/调整 provider、Base URL 或 LiteLLM 路由语义。
11791181
> 审计依据:优先级与回退语义以 `src/config.py` 的 `Config._load_from_env()` 为准(`LITELLM_CONFIG` > `LLM_CHANNELS` > legacy)。配套回归见 `tests/test_llm_channel_config.py`(配置源解析)与 `tests/test_market_review_runtime.py`(共享装配路径)。该接口当前仅提供单进程/单机级防重复能力,若为多实例部署需通过外部任务队列或分布式锁补齐全局幂等。
1182+
> 说明:`POST /api/v1/analysis/market-review` 触发后,报告会以 `report_type=market_review` 写入历史库;你可直接查询 `/api/v1/history` 或 `/api/v1/history/{record_id}` 获取历史 Markdown,避免再次触发分析重算。
11801183
> 说明:该端点若返回 `task_id`,WebUI 会轮询 `GET /api/v1/analysis/status/{task_id}` 展示状态。状态为 `completed` 时给出完成提示(报告已生成并按配置推送),状态为 `failed` 时在前端错误区域显示 `error` 原因。
11811184

11821185
> 兼容性审计证据:

docs/full-guide_EN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ Default schedule: Every weekday at **18:00 (Beijing Time)** automatic execution.
184184
### AI Model Configuration
185185

186186
> Full details: [LLM Config Guide](LLM_CONFIG_GUIDE_EN.md) (three-tier config, channels, Vision, Agent, troubleshooting).
187+
> Compatibility note for Issue #1306: this change only persists and exposes existing market-review output via history paths, and does not alter model name, provider, base URL, LiteLLM cleanup rules, or `.env` runtime migration semantics. Rollback is to revert this change set. Runtime compatibility references are `requirements.txt` (`litellm` constraints), `docs/LLM_CONFIG_GUIDE_EN.md`, and regression tests in `tests/test_analysis_api_contract.py`, `tests/test_analysis_history.py`, `tests/test_market_review.py`; official references: [LiteLLM OpenAI-compatible](https://docs.litellm.ai/docs/providers/openai_compatible), [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat).
187188
188189
| Variable | Description | Default | Required |
189190
|--------|------|--------|:----:|
@@ -1010,6 +1011,7 @@ FastAPI provides RESTful API service for configuration management and triggering
10101011
- **First-run Setup Hint** - The Home page reads the read-only setup status and points users to Settings when required items such as the primary LLM channel or watchlist are missing
10111012
- **Real-time Progress** - Analysis task status updates in real-time, supports parallel tasks; the regular stock-analysis path now prefers LiteLLM streaming during the LLM stage and pushes finer-grained `message/progress` updates through task SSE
10121013
- **Market Review visibility** - After clicking Market Review, the API returns a `task_id` and the UI polls `GET /api/v1/analysis/status/{task_id}` to show progress; completed/failure states are rendered explicitly and failure messages are shown directly in the UI error area.
1014+
- **Market review history replay** - Market review results are persisted with `report_type=market_review` and can be reopened from history list/detail or Markdown endpoints directly, without re-triggering a fresh analysis run.
10131015
- **Backtest Validation** - Evaluate historical analysis accuracy, query direction win rate and simulated returns
10141016
- **API Documentation** - Visit `/docs` for Swagger UI
10151017

@@ -1034,6 +1036,7 @@ FastAPI provides RESTful API service for configuration management and triggering
10341036
> 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.
10351037
> Note: `POST /api/v1/analysis/market-review` follows the same runtime configuration path as CLI/Bot market review (`GeminiAnalyzer(config=...)`, search setup, and prompt/rendering pipeline). The provider compatibility path prioritizes `litellm_model` and `llm_model_list`, then falls back to existing legacy keys (`GEMINI_*`, `OPENAI_*`, `ANTHROPIC_*`, `DEEPSEEK_*`) when those are not set; provider names, Base URL, and LiteLLM routing semantics are otherwise unchanged.
10361038
> Audit note: priority and fallback are defined by `Config._load_from_env()` in `src/config.py` (`LITELLM_CONFIG` > `LLM_CHANNELS` > legacy). Regression coverage is in `tests/test_llm_channel_config.py` (configuration source parsing) and `tests/test_market_review_runtime.py` (shared runtime assembly). The endpoint lock is process/host-level only; multi-instance deployments still need external distributed idempotency controls.
1039+
> Note: Once `/api/v1/analysis/market-review` completes, the report is persisted with `report_type=market_review`; open `/api/v1/history` and `/api/v1/history/{record_id}` (or Markdown history endpoints) to view it directly without re-running analysis.
10371040
> Note: when `/api/v1/analysis/market-review` returns a `task_id`, the WebUI polls `GET /api/v1/analysis/status/{task_id}`. The UI renders clear `pending/processing` progress, shows completion feedback when status becomes `completed`, and surfaces `error` content on `failed`.
10381041

10391042
> Compatibility audit evidence:

0 commit comments

Comments
 (0)