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

767 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<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 目录下引入:
```text
outputs/freshrss/rerun/<run_id>/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_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 为下游编排层的工作流服务。**