diff --git a/README.md b/README.md index 150f09f..69fe154 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,7 @@ python scripts/apply_term_suggestions.py ^ - `ARTICLE_SUMMARY_LLM_API_KEY` 如果这些变量未设置,单篇总结会回退使用主流程中的 `LLM_*` / `OPENAI_*` 配置。 +如果显式指定 DeepSeek 作为单篇总结模型且请求超时或失败,当前实现会自动再用主流程默认模型配置重试一次。 示例(PowerShell 风格): @@ -316,7 +317,8 @@ set ARTICLE_SUMMARY_LLM_MODEL=deepseek-chat python scripts/run_article_summaries.py ^ --extracted outputs/freshrss/extracted/freshrss.extracted.json ^ --ids 12345 67890 ^ - --output-dir outputs/freshrss/single_summaries + --output-dir outputs/freshrss/single_summaries ^ + --timeout 120 ``` 也可以通过 `summary_mcp.server` 暴露的 MCP 工具 `generate_article_summaries` 调用: diff --git a/scripts/run_article_summaries.py b/scripts/run_article_summaries.py index ccd7423..b014824 100755 --- a/scripts/run_article_summaries.py +++ b/scripts/run_article_summaries.py @@ -37,7 +37,7 @@ def main() -> None: help="Directory to write per-article Markdown summaries", ) parser.add_argument("--max-retries", type=int, default=2, help="Maximum LLM retry attempts per article") - parser.add_argument("--timeout", type=float, default=60.0, help="LLM request timeout in seconds") + parser.add_argument("--timeout", type=float, default=120.0, help="LLM request timeout in seconds") parser.add_argument("--api-key", type=str, default=None, help="Override article-summary LLM API key") parser.add_argument("--model", type=str, default=None, help="Override article-summary LLM model") parser.add_argument("--api-url", type=str, default=None, help="Override article-summary LLM API URL/base URL") diff --git a/src/summary_mcp/server.py b/src/summary_mcp/server.py index 05c2f16..6e02a6f 100644 --- a/src/summary_mcp/server.py +++ b/src/summary_mcp/server.py @@ -172,7 +172,7 @@ def generate_article_summaries( selected_ids: list[str], output_dir: str | None = None, max_retries: int = 2, - timeout_seconds: float = 60.0, + timeout_seconds: float = 120.0, llm_api_key: str | None = None, llm_model: str | None = None, llm_api_url: str | None = None, diff --git a/src/summary_mcp/workflows/article_summary.py b/src/summary_mcp/workflows/article_summary.py index 0183d95..98214d5 100644 --- a/src/summary_mcp/workflows/article_summary.py +++ b/src/summary_mcp/workflows/article_summary.py @@ -4,6 +4,8 @@ from dataclasses import dataclass from pathlib import Path from typing import Iterable, Mapping, Sequence +import httpx + from summary_mcp.core.summary_loop import run_loop_payload from summary_mcp.validators.article_summary import validate_article_summary_payload @@ -18,7 +20,7 @@ DEFAULT_PROMPT_PATH = OUTPUT_ROOT / "prompts" / "article-summary-prompt.txt" class ArticleSummaryConfig: prompt_path: Path = DEFAULT_PROMPT_PATH max_retries: int = 2 - timeout_seconds: float = 60.0 + timeout_seconds: float = 120.0 llm_api_key: str | None = None llm_model: str | None = None llm_api_url: str | None = None @@ -187,17 +189,45 @@ def summarize_selected_articles( "warnings": warnings, } - summary_exit_code, summary_payload, _ = run_loop_payload( - extracted_payload=extracted_payload, - prompt_path=cfg.prompt_path, - max_retries=cfg.max_retries, - timeout_seconds=cfg.timeout_seconds, - api_key=resolved_api_key, - model=resolved_model, - api_url=resolved_api_url, - output_path=None, - validator=validate_article_summary_payload, + summary_exit_code = 1 + summary_payload = None + + try: + summary_exit_code, summary_payload, _ = run_loop_payload( + extracted_payload=extracted_payload, + prompt_path=cfg.prompt_path, + max_retries=cfg.max_retries, + timeout_seconds=cfg.timeout_seconds, + api_key=resolved_api_key, + model=resolved_model, + api_url=resolved_api_url, + output_path=None, + validator=validate_article_summary_payload, + ) + except httpx.TimeoutException: + summary_exit_code = 1 + summary_payload = None + + should_fallback = ( + (summary_exit_code != 0 or summary_payload is None) + and resolved_model is not None + and resolved_model == (cfg.llm_model or model or resolved_model) + and resolved_model.startswith("deepseek") ) + + if should_fallback: + summary_exit_code, summary_payload, _ = run_loop_payload( + extracted_payload=extracted_payload, + prompt_path=cfg.prompt_path, + max_retries=cfg.max_retries, + timeout_seconds=cfg.timeout_seconds, + api_key=None, + model=None, + api_url=None, + output_path=None, + validator=validate_article_summary_payload, + ) + if summary_exit_code != 0 or summary_payload is None: continue