# Reader 正式 MCP 服务架构设计 ## 1. 背景 当前 reader 仓库已经具备 MCP 服务入口(`src/summary_mcp/server.py`),并暴露了: - `extract_url_content` - `extract_item_content` - `filter_summary_result` - `run_freshrss_openclaw_pipeline` - `generate_article_summaries` 但从实际生产使用方式看,reader 的正式日报链路仍然**偏向 CLI/脚本模式**,尤其是: - `python scripts/run_freshrss_pipeline.py` - `python scripts/run_article_summaries.py` 这导致在 OpenClaw / Feishu 外层执行环境下,长链路任务容易因为 exec 生命周期、超时或外层中断而失败。 2026-04-06 的事故已经说明: - `raw` 能产出 - `extracted` 能部分产出 - 但 `run-report.json` / `openclaw-delivery-payload.json` / `digest-brief.json` 经常缺失 - 外层日志出现 `signal SIGTERM` 因此,reader 需要从“可被 exec 调起的仓库”升级为“**正式的 MCP 工作流服务**”。 --- ## 2. 设计目标 本次架构设计的目标不是重做 reader 的业务逻辑,而是把现有能力收口为稳定的服务接口。 目标如下: 1. **MCP 成为正式入口** - OpenClaw 与上层编排默认通过 MCP tool 调用 reader - CLI 降级为 debug / fallback 入口 2. **引入稳定的运行态抽象** - 统一 `run_id` - 统一 `status` - 统一 `stage` - 统一 `artifacts` 3. **支持长链路可观测与恢复** - 可查询当前运行状态 - 可查询失败阶段 - 可基于已有中间产物 resume / rerun 4. **让 OpenClaw 消费结构化结果,而不是硬编码目录细节** - OpenClaw 不再依赖 reader 的脚本 stdout 作为唯一信号 - OpenClaw 尽量不直接拼接 reader 的输出目录路径 5. **保持最小重构成本** - 先用文件系统持久化 run state - 先不引入复杂任务队列 / 数据库 / 多 worker 平台 - 先让单机、单实例、顺序执行场景稳定起来 --- ## 3. 核心结论 一句话总结: > Reader 应该被设计为“有状态的工作流 MCP 服务”,而不是“包了一层 MCP 壳的长 CLI 命令”。 换句话说: - **错误方向**:`reader_run(command="python scripts/run_xxx.py ...")` - **正确方向**:`start_run` + `get_run_status` + `get_artifact` + `resume_run` 协议层只是入口,真正的关键是把 reader 内部抽象成: - run - stage - status - artifact - recovery --- ## 4. 服务定位 ### 4.1 Reader 负责什么 reader 作为 MCP 服务,负责上游阅读工作流本身: - FreshRSS 拉取 - 内容提取 - LLM 摘要 - 规则过滤 - candidate / delivery payload 生成 - 单篇总结生成 - 中间产物保存 - 运行状态记录 - 恢复与补跑 ### 4.2 Reader 不负责什么 以下继续由 OpenClaw / skill 层负责: - Hugo 发布 - 对话汇报 - 用户确认精选 - IMA 上传编排 - 最终对人类的日常交互 ### 4.3 边界结论 - **reader 是上游引擎 / workflow service** - **OpenClaw 是下游编排层 / orchestration layer** 这个边界与当前 `docs/openclaw/openclaw-handoff.md` 的原则保持一致,但会进一步强化“reader 通过正式 MCP 接口暴露工作流状态”,而不是只暴露“跑完后的结果”。 --- ## 5. 当前问题分析 ### 5.1 现状问题 当前 reader 的 MCP 工具里虽然已经有 `run_freshrss_openclaw_pipeline`,但其调用语义仍然更像: - 一次性同步执行整个长链路 - 直接返回最终结果 - 对外隐藏中间状态 这会带来几个问题: 1. 长任务运行时,外层必须一直等 2. 一旦外层中断,状态观测困难 3. 难以精确判断失败发生在哪个阶段 4. resume / rerun 只能靠脚本层补丁式处理 5. OpenClaw 不容易构建“先触发,后查询,再消费”的稳定编排流 ### 5.2 根本问题 根本问题不是“有没有 MCP server”,而是: > **reader 还没有被真正建模成一个有状态的 workflow service。** 当前更接近: - MCP 暴露了几个函数 - 但长链路执行模型仍是 CLI thinking 因此改造重点不应放在“多加几个 tool 名字”,而应该放在: - 状态机 - 运行记录 - 恢复机制 - 标准产物注册 --- ## 6. 目标架构 建议的正式架构如下: ```text OpenClaw / Skill Layer -> MCP Client Calls -> Reader MCP Server -> Workflow Service Layer -> Workflow Runtime / Stage Engine -> Existing Reader Core Modules - FreshRSS pull - extraction - summary - filter - candidate builder - article summary -> Run Store (filesystem-backed) -> Artifact Store (existing outputs directory) ``` ### 6.1 分层说明 #### A. MCP Server Layer 职责: - tool 注册 - 输入校验 - 输出包装 - 对外暴露稳定 API 不负责: - 大段业务逻辑 - 状态推进细节 - 复杂流程编排 #### B. Workflow Service Layer 职责: - 接收“启动日报任务”“恢复任务”“查询状态”等请求 - 管理 run 生命周期 - 将调用分发给 runtime/stage engine #### C. Workflow Runtime / Stage Engine 职责: - 执行各阶段 - 记录阶段状态 - 收集中间产物 - 更新运行态文件 - 支持 resume / rerun #### D. Reader Core Modules 继续复用现有业务能力: - `summary_mcp.core.*` - `summary_mcp.workflows.*` - 现有脚本中成熟的逻辑 这层应该尽量保持“业务逻辑纯净”,不要耦合 MCP 协议。 --- ## 7. 核心抽象 ### 7.1 Run 每一次完整的 reader 工作流执行,都对应一个 `run_id`。 建议语义: - `run_id` 是 reader 工作流的一等公民 - 所有状态、产物、恢复都围绕 `run_id` 建立 - 所有下游消费都优先通过 `run_id` 取结果,而不是手拼路径 建议输出目录继续沿用现有结构: ```text outputs/freshrss/rerun// ``` ### 7.2 Stage 建议将 reader 的正式工作流拆为明确阶段: 1. `fetch_feed` 2. `extract_articles` 3. `generate_summaries` 4. `apply_filters` 5. `build_delivery_payload` 6. `write_run_report` 7. `completed` 对于单篇总结链路,可单独建另一类 workflow,或者作为独立 run type。 ### 7.3 Status 建议统一使用: - `queued` - `running` - `partial` - `success` - `failed` - `cancelled` 其中: - `partial` 表示已有阶段成功,但整体尚未完成或部分失败 - `failed` 表示本次 run 已终止且未达到最终成功 ### 7.4 Artifact 所有关键输出都应被注册为 artifact,而不只是“写到了某个目录”。 artifact 至少应包含: - `name` - `path` - `kind` - `stage` - `exists` - `created_at` - `metadata` 关键 artifact 示例: - `raw_output` - `extracted_dir` - `delivery_payload` - `digest_brief` - `run_report` - `single_summary_dir` --- ## 8. 运行态持久化设计 ### 8.1 为什么要单独持久化 run state 仅靠目录中是否存在某些 json 文件,不足以稳定表达: - 当前是否还在跑 - 跑到了哪个阶段 - 哪个阶段失败 - 是否可以 resume - 哪些 artifact 已确认可用 因此必须增加显式的运行态文件。 ### 8.2 建议文件 建议在每个 run 目录下引入: ```text outputs/freshrss/rerun//run-state.json ``` ### 8.3 run-state.json 建议结构 ```json { "run_id": "20260407-093000", "workflow": "freshrss_daily_digest", "run_type": "daily_digest", "status": "running", "current_stage": "extract_articles", "started_at": "2026-04-07T09:30:00+08:00", "updated_at": "2026-04-07T09:31:10+08:00", "finished_at": null, "input": { "limit": 5, "mark_read": true, "include_read": false, "debug_artifacts": false }, "stages": [ { "name": "fetch_feed", "status": "success", "started_at": "2026-04-07T09:30:00+08:00", "finished_at": "2026-04-07T09:30:05+08:00", "outputs": { "pulled_count": 5, "raw_output": "outputs/freshrss/rerun/20260407-093000/raw/freshrss.raw.json" }, "error": null }, { "name": "extract_articles", "status": "running", "started_at": "2026-04-07T09:30:05+08:00", "finished_at": null, "outputs": { "completed_items": 3, "expected_items": 5 }, "error": null } ], "artifacts": [ { "name": "raw_output", "kind": "json", "stage": "fetch_feed", "path": "outputs/freshrss/rerun/20260407-093000/raw/freshrss.raw.json", "exists": true } ], "error": null, "recovery": { "resumable": true, "resume_from_stage": "extract_articles", "last_success_stage": "fetch_feed" } } ``` ### 8.4 设计原则 - 运行中每完成一个 stage,就更新一次 `run-state.json` - 不要求数据库,先以文件系统为准 - 所有对外 status 查询优先读 `run-state.json` - 其他 output 文件仍然可以保留现有格式和目录结构 --- ## 9. MCP Tool 设计 不建议继续把正式能力收口成一个“巨型同步工具”。 建议拆成以下 MCP tools。 ### 9.1 任务启动类 #### `start_freshrss_digest_run` 作用: - 启动一条新的日报工作流 - 默认返回 `run_id`,而不是要求调用方一直同步等待到最后 输入示例: ```json { "limit": 5, "mark_read": true, "include_read": false, "debug_artifacts": false, "timeout_seconds": 60, "max_retries": 2 } ``` 输出示例: ```json { "run_id": "20260407-093000", "status": "queued", "workflow": "freshrss_daily_digest", "output_dir": "outputs/freshrss/rerun/20260407-093000" } ``` #### 兼容策略 第一阶段可保留现有 `run_freshrss_openclaw_pipeline`,但其语义逐步调整为: - 内部复用新的 workflow runtime - 可选 `wait=true/false` - 默认建议 `wait=false` --- ### 9.2 状态查询类 #### `get_run_status` 作用: - 查询 run 当前状态 - 返回 current stage、completed stages、关键 artifact、失败信息 输入: ```json { "run_id": "20260407-093000" } ``` 输出字段建议: - `run_id` - `workflow` - `status` - `current_stage` - `started_at` - `updated_at` - `finished_at` - `progress` - `completed_stages` - `failed_stage` - `error_summary` - `artifacts` - `recovery` #### `list_runs` 作用: - 支持按日期、状态、workflow 类型查看最近 runs 输入示例: ```json { "workflow": "freshrss_daily_digest", "status": "failed", "latest_n": 10 } ``` --- ### 9.3 结果读取类 #### `get_delivery_payload` 作用: - 按 `run_id` 获取 delivery payload - 返回结构化对象,不要求调用方自己读文件 #### `get_digest_brief` 作用: - 按 `run_id` 获取 digest brief - 可支持 `mode=public|internal` #### `get_run_report` 作用: - 返回 run-report 内容 - 供 OpenClaw / 调试 / 运维查看 #### `get_extracted_article` 作用: - 获取单篇 extracted 内容 - 输入:`run_id + item_id` 或 `run_id + item_index` #### `list_run_artifacts` 作用: - 统一列出 run 下当前注册的 artifact --- ### 9.4 恢复与补跑类 #### `resume_run` 作用: - 基于已有 `run-state.json` 和中间产物,从可恢复点继续 输入示例: ```json { "run_id": "20260407-093000" } ``` #### `rerun_stage` 作用: - 指定从某个阶段开始重跑 输入示例: ```json { "run_id": "20260407-093000", "stage": "generate_summaries", "force": true } ``` #### `explain_run_failure` 作用: - 给出适合 OpenClaw / 人类阅读的失败解释 - 不只是 Python stacktrace --- ### 9.5 单篇总结相关工具 现有 `generate_article_summaries` 可继续保留,但建议长期也纳入 run 模型。 后续可扩展为: - `start_article_summary_run` - `get_article_summary_status` - `get_article_summary_outputs` 短期内,如果单篇总结执行耗时可控,也可以先保持同步接口。 --- ## 10. 向后兼容策略 为了降低迁移成本,不建议一次性砍掉现有接口。 ### 10.1 保留现有 MCP 工具 短期保留: - `run_freshrss_openclaw_pipeline` - `generate_article_summaries` 但内部逐步改为调用新的 workflow runtime。 ### 10.2 调整 `run_freshrss_openclaw_pipeline` 语义 建议演进成: - `wait=true` 时:保留当前类似同步行为 - `wait=false` 时:只返回 `run_id` - 若未显式指定,生产建议默认 `wait=false` ### 10.3 CLI 的新定位 CLI 继续保留,但只作为: - debug - fallback - 本地排障 - 开发验证 CLI 最好只是新 runtime 的薄包装,而不是另一套独立实现。 --- ## 11. 与 OpenClaw 的集成方式 ### 11.1 旧模式 OpenClaw: - 直接 exec reader 脚本 - 等待整条 CLI 跑完 - 根据 stdout 或目录文件判断是否成功 ### 11.2 新模式 OpenClaw: 1. 调用 `start_freshrss_digest_run` 2. 获得 `run_id` 3. 周期性调用 `get_run_status` 4. status = `success` 后调用 `get_delivery_payload` 5. 再执行下游 Hugo / chat / IMA 编排 这样会有几个明显好处: - 上游 reader 运行态可观察 - OpenClaw 不必绑死在长 CLI 会话上 - 中途失败可以明确知道失败位置 - 下游编排只依赖结构化结果 --- ## 12. 最小实现方案(MVP) 为了尽快落地,不建议一次做到最重。 ### Phase 1:先做内部状态化 目标: - 在现有 pipeline 内引入 `run_id` - 新增 `run-state.json` - 固化 stage 切分 - 所有关键输出注册成 artifact - 将 resume 所需信息写入 recovery 字段 这一步完成后,即使对外接口还没完全变化,内部也已经不再是“黑箱长函数”。 ### Phase 2:新增 MCP 状态查询接口 目标: - 新增 `get_run_status` - 新增 `get_delivery_payload` - 新增 `list_runs` - 新增 `resume_run` 这一步完成后,OpenClaw 就可以逐步改走正式服务调用。 ### Phase 3:调整现有生产接入 目标: - OpenClaw 默认不再 exec `scripts/run_freshrss_pipeline.py` - OpenClaw 默认走 MCP run + status + payload 模式 - 将 CLI 降级为 debug/fallback --- ## 13. 目录与模块建议 建议新增一层 workflow runtime 模块,例如: ```text src/summary_mcp/ server.py runtime/ run_store.py artifact_store.py state_models.py workflow_service.py stage_runner.py workflows/ freshrss_pipeline.py article_summary.py core/ ... ``` ### 建议职责 - `runtime/state_models.py` - RunState / StageState / Artifact models - `runtime/run_store.py` - 读写 `run-state.json` - `runtime/artifact_store.py` - artifact 注册与查询 - `runtime/workflow_service.py` - start / status / resume / rerun 核心服务 - `runtime/stage_runner.py` - 阶段推进与失败捕获 这样可以保持: - 协议层清晰 - 运行态管理独立 - 业务逻辑复用现有 workflows --- ## 14. 风险与注意事项 ### 14.1 不要把“异步”理解成“一定要上复杂队列” 当前阶段,异步的核心不是上 Redis/Celery,而是: - 有 `run_id` - 有状态文件 - 可以先启动、后查询 单机单进程也完全可以做到。 ### 14.2 不要让 OpenClaw 继续依赖 reader 内部目录细节 OpenClaw 可以知道输出目录存在,但不应继续以“自己拼 `outputs/.../*.json`”作为主交互方式。 正式模式下,优先通过 MCP 取: - status - payload - report - artifact list ### 14.3 不要保留两套行为漂移的实现 如果 CLI 和 MCP 背后各跑各的逻辑,后续一定会漂移。 正确做法: - 先统一 runtime - 再让 CLI / MCP 都调用同一套 runtime --- ## 15. 最终建议 ### 架构判断 Reader 现在已经不再是一个简单脚本仓库,而是: - 有明确上游输入 - 有稳定工作流 - 有中间产物 - 有下游消费者 - 有恢复与补跑需求 因此它应该正式升级为: > **Reader Workflow MCP Service** ### 最终建议清单 1. 把 `run_id / stage / status / artifact / recovery` 作为正式核心抽象 2. 在 `outputs/freshrss/rerun//` 下新增 `run-state.json` 3. 新增 `get_run_status / list_runs / get_delivery_payload / resume_run` 4. 让现有 `run_freshrss_openclaw_pipeline` 内部复用新 runtime 5. 让 OpenClaw 逐步从 exec 切到 MCP 调用 6. CLI 保留,但降级为 debug/fallback --- ## 16. 一句话结论 Reader 的正式生产能力不应再主要依赖长 CLI exec,而应演进为: **以 MCP 为正式入口、以 run-state 为运行真相、以 artifact 为交付契约、以 OpenClaw 为下游编排层的工作流服务。**