Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions api/v1/endpoints/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import json
import logging
import re
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional, Union, Dict, Any
Expand Down Expand Up @@ -116,20 +117,24 @@ def _run_market_review_background(
override_region: Optional[str] = None,
lock_token: Optional[_MarketReviewExecutionLock] = None,
config: Optional[Config] = None,
query_id: Optional[str] = None,
) -> None:
"""Run market review after the API response has been accepted."""
from src.core.market_review import run_market_review

runtime_config = config or get_config_dep()
try:
notifier, analyzer, search_service = _build_market_review_runtime(runtime_config)
report = run_market_review(
notifier=notifier,
analyzer=analyzer,
search_service=search_service,
send_notification=send_notification,
override_region=override_region,
)
review_kwargs = {
"notifier": notifier,
"analyzer": analyzer,
"search_service": search_service,
"send_notification": send_notification,
"override_region": override_region,
}
if query_id:
review_kwargs["query_id"] = query_id
report = run_market_review(**review_kwargs)
if not report:
raise RuntimeError("大盘复盘未返回可持久化报告")
return {"result": report}
Expand Down Expand Up @@ -500,16 +505,19 @@ def trigger_market_review(
)

try:
task_id = uuid.uuid4().hex
task = get_task_queue().submit_background_task(
lambda: _run_market_review_background(
request.send_notification,
override_region=override_region,
lock_token=lock_token,
config=config,
query_id=task_id,
),
stock_code="market_review",
stock_name="大盘复盘",
message="大盘复盘任务已提交",
task_id=task_id,
)
except Exception:
_release_market_review_lock(lock_token)
Expand Down Expand Up @@ -751,6 +759,25 @@ def get_analysis_status(task_id: str) -> TaskStatus:
if records:
record = records[0]
raw_result = parse_json_field(record.raw_result)
if getattr(record, "report_type", None) == "market_review":
market_review_report = None
if isinstance(raw_result, dict):
report_text = raw_result.get("raw_response") or raw_result.get("market_review_report")
if isinstance(report_text, str) and report_text.strip():
market_review_report = report_text
if not market_review_report and record.news_content:
market_review_report = record.news_content

return TaskStatus(
task_id=task_id,
status="completed",
progress=100,
result=None,
market_review_report=market_review_report,
error=None,
stock_name=record.name,
)

model_used = normalize_model_used(
(raw_result or {}).get("model_used") if isinstance(raw_result, dict) else None
)
Expand Down
9 changes: 5 additions & 4 deletions apps/dsa-web/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const HomePage: React.FC = () => {

const reportLanguage = normalizeReportLanguage(selectedReport?.meta.reportLanguage);
const reportText = getReportText(reportLanguage);
const isMarketReviewHistoryReport = selectedReport?.meta.reportType === 'market_review';
const setupNeedsAction = setupStatus ? !setupStatus.isComplete : false;
const setupMissingLabels = useMemo(() => {
if (!setupStatus) {
Expand Down Expand Up @@ -161,7 +162,7 @@ const HomePage: React.FC = () => {
);

const handleAskFollowUp = useCallback(() => {
if (selectedReport?.meta.id === undefined) {
if (selectedReport?.meta.id === undefined || selectedReport.meta.reportType === 'market_review') {
return;
}

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

const handleReanalyze = useCallback(() => {
if (!selectedReport) {
if (!selectedReport || selectedReport.meta.reportType === 'market_review') {
return;
}

Expand Down Expand Up @@ -584,7 +585,7 @@ const HomePage: React.FC = () => {
<Button
variant="home-action-ai"
size="sm"
disabled={isAnalyzing || selectedReport.meta.id === undefined}
disabled={isAnalyzing || selectedReport.meta.id === undefined || isMarketReviewHistoryReport}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable follow-up for market-review history rows

When a persisted market-review row is selected, this new isMarketReviewHistoryReport guard is only applied to the reanalysis button; the adjacent “追问 AI” action still navigates with the synthetic stock=MARKET. ChatPage then calls sanitizeFollowUpStockCode, whose ticker pattern only accepts 1–5 letter US symbols, so MARKET is rejected and the follow-up context is discarded. For market-review history rows, either disable this action too or pass a dedicated market-review context instead of the pseudo-symbol.

Useful? React with 👍 / 👎.

onClick={handleReanalyze}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -595,7 +596,7 @@ const HomePage: React.FC = () => {
<Button
variant="home-action-ai"
size="sm"
disabled={selectedReport.meta.id === undefined}
disabled={selectedReport.meta.id === undefined || isMarketReviewHistoryReport}
onClick={handleAskFollowUp}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand Down
56 changes: 56 additions & 0 deletions apps/dsa-web/src/pages/__tests__/HomePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ const historyReport = {
},
};

const marketReviewHistoryItem = {
id: 2,
queryId: 'market-review-q-1',
stockCode: 'MARKET',
stockName: '大盘复盘',
reportType: 'market_review' as const,
createdAt: '2026-03-18T08:00:00Z',
};

const marketReviewHistoryReport = {
meta: {
id: 2,
queryId: 'market-review-q-1',
stockCode: 'MARKET',
stockName: '大盘复盘',
reportType: 'market_review' as const,
reportLanguage: 'zh' as const,
createdAt: '2026-03-18T08:00:00Z',
},
summary: {
analysisSummary: '大盘复盘摘要',
operationAdvice: '查看复盘',
trendPrediction: '大盘复盘',
sentimentScore: 50,
},
};

describe('HomePage', () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -491,4 +518,33 @@ describe('HomePage', () => {
forceRefresh: true,
}));
});

it('disables stock reanalysis and follow-up for market review history reports', async () => {
vi.mocked(historyApi.getList).mockResolvedValue({
total: 1,
page: 1,
limit: 20,
items: [marketReviewHistoryItem],
});
vi.mocked(historyApi.getDetail).mockResolvedValue(marketReviewHistoryReport);

render(
<MemoryRouter>
<HomePage />
</MemoryRouter>,
);

await screen.findByText('大盘复盘摘要');
const reanalyzeButton = screen.getByRole('button', { name: '重新分析' });
const followUpButton = screen.getByRole('button', { name: '追问 AI' });

expect(reanalyzeButton).toBeDisabled();
expect(followUpButton).toBeDisabled();

fireEvent.click(reanalyzeButton);
fireEvent.click(followUpButton);

expect(analysisApi.analyzeAsync).not.toHaveBeenCalled();
expect(navigateMock).not.toHaveBeenCalled();
});
});
9 changes: 6 additions & 3 deletions apps/dsa-web/src/types/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

// ============ Request Types ============

export type StockReportType = 'simple' | 'detailed' | 'full' | 'brief';
export type ReportType = StockReportType | 'market_review';

export interface AnalysisRequest {
stockCode?: string;
stockCodes?: string[];
reportType?: 'simple' | 'detailed' | 'full' | 'brief';
reportType?: StockReportType;
forceRefresh?: boolean;
asyncMode?: boolean;
stockName?: string;
Expand Down Expand Up @@ -38,7 +41,7 @@ export interface ReportMeta {
queryId: string;
stockCode: string;
stockName: string;
reportType: 'simple' | 'detailed' | 'full' | 'brief';
reportType: ReportType;
reportLanguage?: ReportLanguage;
createdAt: string;
currentPrice?: number;
Expand Down Expand Up @@ -206,7 +209,7 @@ export interface HistoryItem {
queryId: string; // Linked analysis query ID
stockCode: string;
stockName?: string;
reportType?: string;
reportType?: ReportType;
sentimentScore?: number;
operationAdvice?: string;
createdAt: string;
Expand Down
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- [修复] 调高基本面聚合默认超时预算,降低 Windows/Docker 环境下整段基本面 timeout 的概率。
- [修复] 正式分析链路兼容 OpenAI-compatible `content_blocks` 响应,避免 `message.content=null` 时被误判为空回复。
- [文档] 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` 回退逻辑避免兼容断层。
- [文档] 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`。
- [改进] 大盘复盘新增 `MARKET_REVIEW_COLOR_SCHEME` 配置,可在指数涨跌幅中选择绿涨红跌或红涨绿跌。
- [文档] 明确 `MARKET_REVIEW_COLOR_SCHEME` 仅为大盘复盘展示配置,枚举为 `green_up`/`red_up`(默认 `green_up`),属于文案与颜色语义层面变更;本次未调整模型名、provider、Base URL、LLM 运行时迁移或运行时清理逻辑。
- [修复] 大盘复盘执行结果写入现有分析历史,Web 历史列表可直接查看已生成复盘,避免重复触发分析。

## [3.16.0] - 2026-05-10

Expand Down
3 changes: 3 additions & 0 deletions docs/full-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ daily_stock_analysis/
### AI 模型配置

> 完整说明见 [LLM 配置指南](LLM_CONFIG_GUIDE.md)(三层配置、渠道模式、Vision、Agent、排错);常用服务商预设、Actions 变量对照和错误排障见 [LLM 服务商配置指南](llm-providers.md)。
> 兼容性说明(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)。
> 本节仅同步模型/渠道配置清单,不额外引入新的外部 provider / Base URL 兼容约定;兼容语义以当前仓库 `requirements.txt` 依赖约束和相关测试为准,历史回退路径见上述两份文档中“回退/恢复”说明。

| 变量名 | 说明 | 默认值 | 必填 |
Expand Down Expand Up @@ -1151,6 +1152,7 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
- 🧭 **首次配置提示** - 首页会读取只读配置状态,缺少 LLM 主渠道、自选股等基础项时提示缺口并引导进入系统设置
- 📊 **实时进度** - 分析任务状态实时更新,支持多任务并行;普通分析链路在进入 LLM 阶段后会优先尝试 LiteLLM 流式生成,并通过任务 SSE 回灌更细粒度的 `message/progress`
- 🗂️ **大盘复盘任务可见性** - 首页触发大盘复盘后会返回 `task_id` 并轮询 `GET /api/v1/analysis/status/{task_id}`,在进行中/完成/失败场景给出可见反馈,失败时直接透出报错内容
- 🧾 **市场复盘历史可复用** - 大盘复盘任务会持久化到分析历史,`report_type` 为 `market_review`,可直接通过历史列表/详情打开对应 Markdown 或详情页,不会重新触发分析重算
- 📈 **回测验证** - 评估历史分析准确率,查询方向胜率与模拟收益
- 🔗 **API 文档** - 访问 `/docs` 查看 Swagger UI

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

> 兼容性审计证据:
Expand Down
3 changes: 3 additions & 0 deletions docs/full-guide_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ Default schedule: Every weekday at **18:00 (Beijing Time)** automatic execution.
### AI Model Configuration

> Full details: [LLM Config Guide](LLM_CONFIG_GUIDE_EN.md) (three-tier config, channels, Vision, Agent, troubleshooting).
> 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).

| Variable | Description | Default | Required |
|--------|------|--------|:----:|
Expand Down Expand Up @@ -1010,6 +1011,7 @@ FastAPI provides RESTful API service for configuration management and triggering
- **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
- **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
- **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.
- **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.
- **Backtest Validation** - Evaluate historical analysis accuracy, query direction win rate and simulated returns
- **API Documentation** - Visit `/docs` for Swagger UI

Expand All @@ -1034,6 +1036,7 @@ FastAPI provides RESTful API service for configuration management and triggering
> 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.
> 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.
> 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.
> 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.
> 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`.

> Compatibility audit evidence:
Expand Down
Loading
Loading