16 KiB
Reader 正式 MCP 服务架构设计
1. 背景
当前 reader 仓库已经具备 MCP 服务入口(src/summary_mcp/server.py),并暴露了:
extract_url_contentextract_item_contentfilter_summary_resultrun_freshrss_openclaw_pipelinegenerate_article_summaries
但从实际生产使用方式看,reader 的正式日报链路仍然偏向 CLI/脚本模式,尤其是:
python scripts/run_freshrss_pipeline.pypython 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 的业务逻辑,而是把现有能力收口为稳定的服务接口。
目标如下:
-
MCP 成为正式入口
- OpenClaw 与上层编排默认通过 MCP tool 调用 reader
- CLI 降级为 debug / fallback 入口
-
引入稳定的运行态抽象
- 统一
run_id - 统一
status - 统一
stage - 统一
artifacts
- 统一
-
支持长链路可观测与恢复
- 可查询当前运行状态
- 可查询失败阶段
- 可基于已有中间产物 resume / rerun
-
让 OpenClaw 消费结构化结果,而不是硬编码目录细节
- OpenClaw 不再依赖 reader 的脚本 stdout 作为唯一信号
- OpenClaw 尽量不直接拼接 reader 的输出目录路径
-
保持最小重构成本
- 先用文件系统持久化 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,但其调用语义仍然更像:
- 一次性同步执行整个长链路
- 直接返回最终结果
- 对外隐藏中间状态
这会带来几个问题:
- 长任务运行时,外层必须一直等
- 一旦外层中断,状态观测困难
- 难以精确判断失败发生在哪个阶段
- resume / rerun 只能靠脚本层补丁式处理
- 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 的正式工作流拆为明确阶段:
fetch_feedextract_articlesgenerate_summariesapply_filtersbuild_delivery_payloadwrite_run_reportcompleted
对于单篇总结链路,可单独建另一类 workflow,或者作为独立 run type。
7.3 Status
建议统一使用:
queuedrunningpartialsuccessfailedcancelled
其中:
partial表示已有阶段成功,但整体尚未完成或部分失败failed表示本次 run 已终止且未达到最终成功
7.4 Artifact
所有关键输出都应被注册为 artifact,而不只是“写到了某个目录”。
artifact 至少应包含:
namepathkindstageexistscreated_atmetadata
关键 artifact 示例:
raw_outputextracted_dirdelivery_payloaddigest_briefrun_reportsingle_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_idworkflowstatuscurrent_stagestarted_atupdated_atfinished_atprogresscompleted_stagesfailed_stageerror_summaryartifactsrecovery
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_runget_article_summary_statusget_article_summary_outputs
短期内,如果单篇总结执行耗时可控,也可以先保持同步接口。
10. 向后兼容策略
为了降低迁移成本,不建议一次性砍掉现有接口。
10.1 保留现有 MCP 工具
短期保留:
run_freshrss_openclaw_pipelinegenerate_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:
- 调用
start_freshrss_digest_run - 获得
run_id - 周期性调用
get_run_status - status =
success后调用get_delivery_payload - 再执行下游 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
最终建议清单
- 把
run_id / stage / status / artifact / recovery作为正式核心抽象 - 在
outputs/freshrss/rerun/<run_id>/下新增run-state.json - 新增
get_run_status / list_runs / get_delivery_payload / resume_run - 让现有
run_freshrss_openclaw_pipeline内部复用新 runtime - 让 OpenClaw 逐步从 exec 切到 MCP 调用
- CLI 保留,但降级为 debug/fallback
16. 一句话结论
Reader 的正式生产能力不应再主要依赖长 CLI exec,而应演进为:
以 MCP 为正式入口、以 run-state 为运行真相、以 artifact 为交付契约、以 OpenClaw 为下游编排层的工作流服务。