Files
reader/plans/reader-mcp-architecture-design.md
T

16 KiB
Raw Blame History

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. 目标架构

建议的正式架构如下:

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 取结果,而不是手拼路径

建议输出目录继续沿用现有结构:

outputs/freshrss/rerun/<run_id>/

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 目录下引入:

outputs/freshrss/rerun/<run_id>/run-state.json

8.3 run-state.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,而不是要求调用方一直同步等待到最后

输入示例:

{
  "limit": 5,
  "mark_read": true,
  "include_read": false,
  "debug_artifacts": false,
  "timeout_seconds": 60,
  "max_retries": 2
}

输出示例:

{
  "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、失败信息

输入:

{
  "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

输入示例:

{
  "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 和中间产物,从可恢复点继续

输入示例:

{
  "run_id": "20260407-093000"
}

rerun_stage

作用:

  • 指定从某个阶段开始重跑

输入示例:

{
  "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 模块,例如:

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_id>/ 下新增 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 为下游编排层的工作流服务。