feat: add async resume jobs and doc navigation

This commit is contained in:
root
2026-04-14 15:55:02 +08:00
parent 6705613aa4
commit b8727f1885
26 changed files with 2791 additions and 665 deletions
+9
View File
@@ -0,0 +1,9 @@
# OpenClaw 历史归档
本目录只保留阶段性总结、设计演进记录和排障计划。
使用原则:
- 需要了解“为什么会这样设计”时再看
- 不要把这里的描述当成当前生产事实
- 当前正式口径以 `docs/openclaw/README.md`、`docs/openclaw/openclaw-handoff.md`、`docs/openclaw/openclaw-orchestration-flow.md` 为准
@@ -0,0 +1,314 @@
# Digest Optimization Summary
## 背景
reader → OpenClaw 日报链路原先的问题主要有两类:
1. **OpenClaw public digest 输入过重**
- public digest 直接读取完整 `openclaw-delivery-payload.json`
- 其中混有大量不直接服务公开日报的字段
- public digest 这一步在 OpenClaw 侧消耗了较多 token
2. **public / internal 生成逻辑没有充分拆分**
- public digest 与 internal review digest 都基于完整 payload 推导
- 容易造成重复消耗
- public digest 还可能被 review / 内部流程语义污染
本轮优化的目标不是重写 reader 主流程,而是在不破坏现有 delivery payload 的前提下,先把 public digest 的输入和生成方式收敛下来,并验证整体 token 与内容质量的变化。
---
## 本轮改动
### 1. reader 新增 `digest-brief.json`
在 FreshRSS pipeline 写出:
- `outputs/freshrss/rerun/<run-id>/candidates/openclaw-delivery-payload.json`
之后,额外生成:
- `outputs/freshrss/rerun/<run-id>/candidates/digest-brief.json`
用途:
- 供 OpenClaw 生成 **public digest** 时优先读取
- 作为 public-only 的轻量输入视图
当前约束:
- 仅保留 `selection_decision == "keep"` 的候选
- 默认最多保留前 5 条
- 高亮 `highlights` 最多保留 3 条
- schema 标识为 `digest-brief.v1`
保留字段:
- `title`
- `source_name`
- `summary`
- `highlights`
- `category`
- `digest_rank`
- `selection_decision`
- `url`
附带计数:
- `source_candidate_count`
- `candidate_count`
---
### 2. public / internal 输入边界拆分
当前推荐口径:
- **public digest**
- 优先读取 `digest-brief.json`
- 仅使用 public-only 输入视图
- **internal review digest**
- 继续读取完整 `openclaw-delivery-payload.json`
- 保留 keep / review 的决策上下文
这样做的原因:
- public digest 需要更轻、更干净的公开输入
- internal review digest 仍然需要完整上下文来支撑判断、待确认与建议沉淀
---
### 3. digest 成稿的正式落盘位置
正式 run 生成出的 digest 成稿,不应停留在 OpenClaw 的临时目录,而应回写到同一次 reader run 目录下。
推荐正式产物位置:
- `outputs/freshrss/rerun/<run-id>/digest/public_digest.md`
- `outputs/freshrss/rerun/<run-id>/digest/internal_review_digest.md`
- `outputs/freshrss/rerun/<run-id>/digest/combined.json`
这样可以保证:
- 一次 run 的所有输入、输出、摘要结果和日报成稿都收在同一目录下
- 后续 Hugo 发布与 chat 回传基于同一组正式产物,而不是临时文件
- 便于回溯、复盘和后续自动化收口
### 4. 一次生成两份 digest 的生成模式
推荐把:
- `public digest`
- `internal review digest`
改为在 OpenClaw 侧 **一次调用同时生成两份**。
推荐输入:
- `PUBLIC_DIGEST_INPUT` → `digest-brief.json`
- `INTERNAL_REVIEW_INPUT` → `openclaw-delivery-payload.json`
推荐输出:
```json
{
"public_digest_markdown": "...",
"internal_review_digest_markdown": "..."
}
```
这样可以减少重复 prompt / 调用开销,同时保留 public / internal 两种视图的边界。
---
### 5. internal review digest 风格收敛
internal review digest 经过一轮人工验证后,收敛成以下规则:
固定结构:
1. `今日候选概况`
2. `已入选重点`
3. `待你确认`
4. `建议沉淀到 IMA`
5. `原始候选清单`
表达规则:
- 不显示 `rank`
- 不显示英文 machine state
- 使用中文状态:
- `keep` → `已入选`
- `review` → `待确认`
- `drop` → `暂不纳入`
内容规则:
- `已入选重点`
- 标题 + 来源
- 状态
- 较完整的一段摘要
- 一段判断(解释为什么值得入选,以及它在今天 digest 中承担什么角色)
- `待你确认`
- 标题 + 来源
- 状态
- 较完整的一段摘要
- 原因
- 建议
- `原始候选清单`
- 也用中文状态,而不是 `decision=keep/review`
目标:
- 保留 internal review digest 作为“给人看的审阅稿”的属性
- 避免它沦为 payload 的原样转写或机器中间态展示
---
### 6. public digest 风格收敛
public digest 当前推荐结构:
1. `今日概览`
2. `今日重点`
3. `趋势观察`
4. `延伸阅读`
5. `信息来源`
表达规则:
- 不暴露 internal workflow 词汇
- 不写 `待确认` / `建议沉淀到 IMA` / `keep/review/drop` / `selection_decision`
- 保持适合 Hugo 公开浏览的表达方式
内容规则:
- 每个 `今日重点` 条目除了摘要和 highlights 外,增加一句编辑性总结
- 推荐形式:
- `这篇内容更值得关注的原因在于……`
目标:
- 保证 public digest 不只是“摘要列表”
- 而是一份带有编辑性提炼的公开日报
---
## 实测结果
基于真实 run:
- run 目录:`outputs/freshrss/rerun/20260401-074614`
### 1. public 输入压缩效果
- 完整 payload:`8701` 字符
- `digest-brief.json`:`3080` 字符
压缩比例:
- **减少约 64.6%**
按中位 token 粗估:
- 完整 payload:约 `3955 tokens`
- public brief:约 `1400 tokens`
public 输入侧单次大约减少:
- **约 2500 tokens**
---
### 2. 一次生成两份的总成本估算
基于真实输入输出的中位估算:
- 旧方案(两次生成):约 `10328 tokens`
- 新方案(一次生成两份):约 `7734 tokens`
节省:
- **约 2594 tokens**
- **约 25%**
说明:
- 第一步 public 输入瘦身带来的是“输入量级下降”
- 第二步一次生成两份带来的是“调用层重复开销下降”
- 两者叠加后,已经形成比较明显的成本优化效果
---
## 当前默认口径
### public digest
- 输入:`digest-brief.json`
- 风格:公开浏览稿
- 每个重点项包含:
- 摘要
- 关键信号
- 一句编辑性总结
### internal review digest
- 输入:完整 payload
- 风格:内部审阅稿
- 每个重点项包含:
- 更完整摘要
- 判断
- 每个待确认项包含:
- 更完整摘要
- 原因
- 建议
### 生成方式
- 优先采用 **一次调用同时生成两份**
---
## 当前阶段结论
本轮优化已经形成一个可用版本:
- reader 新增 public-only 轻量输入视图
- public / internal 边界清楚
- internal 风格和 public 风格都收敛到了可接受版本
- token 成本下降有明确实测支撑
- skill 文档与流程规范已经同步更新
当前更适合的策略不是继续抽象设计,而是:
- 按这套新流程再跑几次真实日报
- 观察稳定性、质量波动和实际使用感受
---
## 后续可选方向
### 1. internal 输入进一步轻量化
潜在方向:
- 新增一个 internal 专用的轻量视图
- 但需要谨慎,避免削弱 internal review 的判断价值
### 2. digest 阶段模型分层
潜在方向:
- reader 上游继续用便宜模型做抽取和结构化
- OpenClaw digest 阶段单独切到更便宜或更合适的模型
### 3. 自动化执行收口
潜在方向:
- 把“一次生成两份”的逻辑进一步标准化
- 更顺滑地接 Hugo 发布与聊天回传
- 让 reader → OpenClaw → Hugo / chat 的路径更接近真正的稳定生产流程
@@ -0,0 +1,163 @@
# reader MCP workflow service formalization summary (2026-04-07)
## Overview
On 2026-04-07, the reader project was formally advanced from a script-first integration model into a reader-centric MCP workflow service model.
The key shift is:
- before: OpenClaw primarily relied on long CLI / exec flows and direct output-path stitching
- now: reader exposes a formal workflow-oriented MCP surface with run-state, status queries, result reads, and minimal recovery
This document records the main outcomes and commits for the first formalization phase.
---
## Completed capability set
### 1. Run-state persistence
Commit:
- `72a6853` — `Add run-state persistence for FreshRSS pipeline`
Delivered:
- `run-state.json`
- `RunState / StageState / ArtifactRecord`
- stage-level state persistence for the FreshRSS pipeline
### 2. Architecture / implementation docs
Commit:
- `7563aa8` — `docs: add reader MCP architecture and implementation plan`
Delivered:
- architecture design
- implementation plan
- TODO-driven collaboration model
### 3. MCP run-status query tools
Commit:
- `d9173fb` — `feat: add MCP run status query tools`
Delivered:
- `get_run_status`
- `list_runs`
- `list_run_artifacts`
### 4. MCP result-read tools
Commit:
- `4a02894` — `Add MCP delivery payload and run report queries`
Delivered:
- `get_delivery_payload`
- `get_run_report`
### 5. Minimal resume design
Commit:
- `d91cdbc` — `docs: narrow resume_run minimal recovery design`
Delivered:
- narrowed design for `resume_run`
- explicit supported / unsupported recovery points
### 6. Minimal `resume_run`
Commit:
- `c622bc6` — `Implement minimal resume_run for freshrss runs`
Delivered:
- minimal `resume_run`
- supports only freshrss runs with `run-state.json`
- supports only recent resumable points
- explicitly rejects `fetch_feed` and `extract_articles`
### 7. Formal handoff / workflow docs
Commits:
- `4f219ef` — `docs: formalize reader MCP workflow service handoff`
- `2df0af5` — `docs: add openclaw orchestration flow for reader MCP`
Delivered:
- formal handoff aligned to actual implementation
- OpenClaw orchestration runbook
- explicit rule that OpenClaw should stop hand-stitching reader paths in the normal production flow
---
## Current formal MCP workflow surface
The current reader MCP workflow surface now includes:
- `run_freshrss_openclaw_pipeline`
- `get_run_status`
- `list_runs`
- `list_run_artifacts`
- `get_delivery_payload`
- `get_run_report`
- `resume_run` (minimal version)
---
## Current boundary
reader is now the upstream workflow engine for:
- FreshRSS pull
- extraction
- summary
- filter
- payload generation
- run-state persistence
- result read
- minimal recovery
OpenClaw / skill remains responsible for:
- digest markdown generation
- Hugo publishing
- chat reporting
- user confirmation
- IMA orchestration
---
## Current limitations
The first formalization phase is complete, but some constraints remain:
- `resume_run` is still minimal and does not support arbitrary stage re-entry
- historical runs without `run-state.json` are not formally recoverable
- some very old runs may still require conservative artifact/path discovery
- `rerun_stage` is not implemented
- deeper runtime consolidation of `run_freshrss_openclaw_pipeline` can still be improved later
---
## Practical conclusion
The reader project should now be treated as a formal MCP workflow service rather than as a long-running CLI-first integration point.
For normal production orchestration:
- start via MCP
- observe via MCP status tools
- read results via MCP result tools
- use `resume_run` only within the documented minimal recovery range
- keep CLI for debug / fallback only
@@ -0,0 +1,222 @@
# OpenClaw 日报聚合改造说明
## 1. 为什么要改
当前项目已经跑通了:
`FreshRSS -> item -> extraction -> LLM summary -> validator -> filter -> Markdown sink`
这条链路证明了上游拉取、结构化提取、摘要校验和规则过滤都是可行的。
但如果最终目标是:
- 每日提炼总结进入知识库
- OpenClaw 向用户汇报今天的日报
那么当前“单篇过滤后直接写入本地 Markdown 知识库”的主路径就不够准确了。
原因有四点:
1. 单篇文章更像原材料,不是最终成品
2. 用户要的是“日报级总结 + 知识沉淀”,不是“每篇都直接入库”
3. 参考文章中真正的终态是 `Digest -> Daily Review -> 人工确认 -> Lumina`
4. 当前 `article_candidate` 如果同时承担内部记录和下游投递,会导致 payload 过重、token 过高、边界混乱
所以当前项目要从“单篇直接入库”切换为“单篇候选材料 -> 日报聚合 -> 人工确认 -> 知识沉淀”。
## 2. 参考文章给出的真实结构
参考文章里的关键分层是:
1. `Digest`
- 去重、抓正文、质量检查、生成摘要
- 产出可判断的候选内容
2. `Daily Review`
- 从候选内容中做栏目化精选
- 产出当天的日报
3. `Human in the loop`
- 人工判断哪些内容值得长期保留
4. `Lumina`
- 只承接真正值得长期保留的内容
这意味着:
- 日报不是知识库的替代品
- 知识库也不应该承接所有单篇文章
- 单篇内容更适合作为日报生成前的候选材料
## 3. 当前设计哪里不够
当前的不足主要在下游:
### 3.1 `sink` 语义过重
现在的 `Markdown sink` 更像“最终入库器”。
但在新的目标里,它最多只应扮演:
- debug sink
- fallback sink
- 审计/归档 sink
主路径不应再把它视为最终知识库形态。
### 3.2 缺少“日级对象”
当前项目有:
- `item`
- `article`
- `summary`
- `filter_decision`
但还没有:
- `ArticleCandidateRecord`
- `OpenClawCandidateInput`
- `DailyDigest`
如果没有这三个对象,就无法稳定表达:
- 单篇内容如何作为内部候选材料存在
- 单篇内容如何以低 token 的形式发给 OpenClaw
- 一天的内容如何被聚合为一个正式产物
### 3.3 缺少人工确认层
规则引擎的 `keep` 只能表示“值得进入下一步”,不能直接等价于“正式入知识库”。
如果没有人工确认层,系统就会退化成“规则命中即永久沉淀”,这和参考文章强调的 `Human in the loop` 不一致。
### 3.4 下游输入边界不清楚
如果把正文全文、完整规则证据链、本地文件路径都发给 OpenClaw,会出现三个直接问题:
- token 浪费
- 对象职责混乱
- OpenClaw 输入与本地实现耦合
所以必须把“内部记录对象”和“下游精简输入对象”分开。
## 4. 改造后的主路径
建议主路径调整为:
`FreshRSS/RSS -> extraction -> LLM summary -> validator -> rule engine -> ArticleCandidateRecord -> OpenClawCandidateInput -> OpenClaw -> DailyDigest -> human review -> knowledge base`
这里要注意几件事:
1. 当前项目负责生成高质量候选材料与精简输入
2. OpenClaw 负责按天聚合、生成日报、编排后续动作
3. 知识库只接收日报和人工确认后的长期内容
## 5. 新的对象分层
### 5.1 `ArticleCandidateRecord`
定位:
- 单篇内容的内部候选记录
- 用于留档、追溯、重放、审计
最少应包含:
- `item`
- `article`
- `summary`
- `filter_result`
- `metadata`
- 可选 `source_refs`
- 可选 `rendered_markdown`
### 5.2 `OpenClawCandidateInput`
定位:
- 当前项目发给 OpenClaw 的标准投递对象
- 用于日报聚合与排序
最少应包含:
- `candidate_id`
- `title`
- `url`
- `published_at`
- `author`
- `summary`
- `highlights`
- `topics`
- `category`
- `worth_keeping`
- `selection_decision`
- `selection_reason`
- `digest_section_hint`
- `digest_rank`
它不应包含:
- `article.plain_text`
- `filter_result.matches`
- `source_refs`
### 5.3 `DailyDigest`
定位:
- 一天的主产物
- 同时服务于“知识沉淀”和“日报汇报”
建议至少包含:
- `date`
- `sections`
- `top_items`
- `key_takeaways`
- `watchlist`
- `candidate_ids`
- `source_refs`
- `editor_notes`
## 6. 为什么这样更合理
这样改造有几个直接收益:
1. 知识库不会被大量单篇摘要污染
2. OpenClaw 不需要再次消费全文,token 更可控
3. OpenClaw 的输入边界更清晰,不依赖本地目录结构和规则证据链
4. 当前项目仍然保留完整候选记录,因此误判排查和重放能力不会丢
5. 未来扩展周报、专题文章和反馈回流会更自然
本质上,这是把当前系统从“单篇落库工具”调整为“日报生产链路中的候选记录生产器 + 精简投递器”。
## 7. 对当前实现的影响
不需要推翻已有能力,主要是重新定位:
- `extraction` 保留
- `LLM summary validator` 保留
- `rule engine` 保留
- `FreshRSS integration` 保留
- `Markdown sink` 保留,但降级为调试/回退能力
真正新增的是:
- `ArticleCandidateRecord` schema
- `OpenClawCandidateInput` schema
- `ArticleCandidateRecord -> OpenClawCandidateInput` 映射逻辑
- `DailyDigest` schema
- 人工确认后的状态流转
## 8. 下一步应该做什么
建议按这个顺序推进:
1. 在代码里把当前 `article_candidate` 重新定位为 `ArticleCandidateRecord`
2. 新增 `OpenClawCandidateInput` 模型
3. 增加精简 payload 的本地输出脚本或转换函数
4. 让 OpenClaw 只消费精简输入对象
5. 再设计批量聚合和 `DailyDigest` 生成
## 9. 一句话结论
如果最终目标是“每天有日报汇报,同时把日级提炼沉淀进知识库”,那么当前项目就不该继续围绕“单篇直接入库”演进,而应拆成“内部候选记录对象 + OpenClaw 精简输入对象 + 日级成品对象”三层结构。
@@ -0,0 +1,367 @@
# Reader 日报链路 P1 状态收敛问题:规划与修复清单(2026-04-14)
## 背景
在 2026-04-14 的 reader 日报正式运行中,出现了以下现象:
- `openclaw-delivery-payload.json`、`digest-brief.json`、`run-report.json` 已真实落盘
- 但 `get_freshrss_pipeline_job_status` / `get_run_status` 仍可能显示:
- `running`
- `failed`
- 或 `current_stage=generate_summaries`
- `resume_run` 在这种状态下可能直接超时
这说明当前 reader 的**状态层(job/run-state)**与**产物层(artifacts/report)**之间没有稳定收敛。
---
## 本次确认的核心结论
### 1. job status 与 run status 是两套独立状态系统
- **job 层状态**:`src/summary_mcp/runtime/freshrss_pipeline_jobs.py`
- `start_freshrss_pipeline_job()`
- `run_freshrss_pipeline_job()`
- `get_freshrss_pipeline_job_status()`
- 状态文件位于:`outputs/freshrss/pipeline_jobs/<job_id>/run-state.json`
- 只有 4 个粗粒度 stage:
- `prepare_job`
- `load_input`
- `run_pipeline`
- `write_result`
- **run 层状态**:`src/summary_mcp/workflows/freshrss_pipeline.py`
- `run_freshrss_pipeline()`
- 由 `src/summary_mcp/runtime/query_service.py:get_run_status()` 查询
- 状态文件位于:`outputs/freshrss/rerun/<run_dir>/run-state.json`
- 包含 6 个细粒度 stage:
- `fetch_feed`
- `extract_articles`
- `generate_summaries`
- `apply_filters`
- `build_delivery_payload`
- `write_run_report`
**问题:** 两套状态没有统一收敛规则,用户可以同时看到两套不同口径的“当前进度”。
---
### 2. 查询层目前优先信 run-state,不会用 artifacts / run-report 纠偏
代码位置:`src/summary_mcp/runtime/query_service.py`
关键行为:
- `_resolve_run_record()` 只要发现 `run-state.json` 存在,就优先使用 `RunStore.load(...)`
- 即使 `run-report.json`、`delivery_payload`、`digest_brief` 已存在,也不会自动纠偏状态
**结果:**
- 一旦 `run-state.json` 因中断、超时、外层 SIGTERM 或写回未完成而停留在旧值
- `get_run_status()` 就会持续返回过期状态
- 造成“产物已完成,但状态仍显示 running/failed/卡在 summary”的错觉
---
### 3. `generate_summaries` 假卡住,本质上更像 stale state,不像真实业务卡住
代码位置:`src/summary_mcp/workflows/freshrss_pipeline.py`
从执行顺序看:
1. `start_stage(generate_summaries)`
2. summary 循环
3. `finish_stage(generate_summaries)`
4. `start_stage(apply_filters)`
5. `finish_stage(apply_filters)`
6. `start_stage(build_delivery_payload)`
7. 写 payload / digest brief
8. `finish_stage(build_delivery_payload)`
9. `start_stage(write_run_report)`
10. 写 run-report
11. `finish_stage(write_run_report)`
12. `finish_run(...)`
**判断:**
如果 payload / digest brief / run-report 都已经存在,那么“仍显示卡在 `generate_summaries`”更可能是:
- `run-state.json` 没来得及写回最终状态
- 或查询时读到了旧状态
而不是 summary 阶段真实没有跑过去。
---
### 4. `resume_run` 不是轻量恢复,而是同步继续跑工作流
代码位置:`src/summary_mcp/runtime/resume_service.py`
关键行为:
- `resume_run()` 会根据 `resume_from_stage` 直接继续执行:
- `_run_summary_stage(...)`
- `_run_filter_stage(...)`
- `_run_delivery_stage(...)`
- `_run_report_stage(...)`
这意味着它不是“修状态”的工具,而是“同步继续跑剩余工作流”的工具。
**问题:**
- 如果 stale state 把 `resume_from_stage` 定在 `generate_summaries`
- 那么 `resume_run` 会从一个过早阶段重新跑
- 在 MCP 包装层下非常容易超时
---
## 问题分类
### A. 真实 bug
1. **查询层过度信任 stale `run-state.json`**
- 文件:`src/summary_mcp/runtime/query_service.py`
- 影响:产物已完成但状态仍错误
2. **`resume_run` 过度依赖 stale `current_stage` / recovery 信息**
- 文件:`src/summary_mcp/runtime/resume_service.py`
- 影响:从过早阶段重跑,放大 timeout 风险
### B. 状态设计缺陷
3. **job 层与 run 层两套状态源没有统一收敛规则**
- 文件:`src/summary_mcp/runtime/freshrss_pipeline_jobs.py`
- 文件:`src/summary_mcp/runtime/query_service.py`
- 影响:用户看到两个互相打架的状态解释
4. **状态系统完全依赖显式写回,不会按产物反推修正**
- 文件:`src/summary_mcp/runtime/run_store.py`
- 影响:一旦中断,状态比产物更容易脏
### C. 调用层误判
5. **把 `resume_run` 当成轻量恢复接口使用**
- 实际上它更接近“同步恢复执行器”
- 影响:在长链路场景下超时是高概率事件
---
## 修复目标
## 当前落地状态(回填)
- [x] Phase 1 已落地:`get_run_status()` 会基于 `run-report.json` 与关键产物做终态收敛,并暴露 `status_source` / `state_conflict`
- [x] Phase 2 已落地第一阶段:`resume_run()` 会拒绝对已有终态 `run-report.json` 的 run 继续恢复
- [x] Phase 2 已继续增强:恢复起点现在会优先根据 artifacts 重算,而不是直接盲信 `run-state.recovery.resume_from_stage`
- [x] 新增 `inspect_resume_plan(run_id)` 作为恢复前置判定接口,避免调用方用 `resume_run` 探路
- [x] Phase 2 已补齐生产恢复 artifacts:正式 run 会稳定写出 `summary/summary-batch.json` 与 `candidates/candidate-batch.json`,`resume_run` / `inspect_resume_plan` 会优先使用它们,而不是依赖 debug per-item 文件
- [x] Phase 3 已落地:job 状态与结果读取会基于 linked run 做收敛,避免 outer job stale state 卡住编排
### 一级目标(必须达成)
1. 当 `run-report.json` / `delivery_payload` / `digest_brief` 已存在时,`get_run_status()` 不应继续盲目展示明显过期的 stage 状态;对调用方暴露的 `status` 必须直接收敛为可用终态,而不是只附加 hint
2. 当状态层与产物层冲突时,查询结果必须显式标注“状态冲突 / stale state”
3. `resume_run()` 在恢复前应优先基于现有 artifacts 判断真实可恢复起点,避免从过早阶段重跑
### 二级目标(建议达成)
4. job 层状态结果中增加对 linked run 的补充解释,避免“job running 但 run 产物已齐”这种情况毫无说明
5. 为后续编排层提供明确可消费的“状态可信度/冲突提示”字段
---
## 最小修复方案
### Phase 1|先修 run 查询层(优先级最高)
#### 目标
让 `get_run_status()` 至少能正确识别:
- run-state 是旧的
- 但关键产物已经齐了
#### 建议改动点
文件:`src/summary_mcp/runtime/query_service.py`
#### 建议动作
- [x] 在 `_resolve_run_record()` 或 `_build_status_response()` 中增加“关键产物存在性检查”
- `run-report.json`
- `candidates/openclaw-delivery-payload.json`
- `candidates/digest-brief.json`
- [x] 如果 `run-state.current_stage` 仍停留在早期阶段,但关键产物已齐:
- 不要继续原样输出为可信最终态
- 应直接把对外 `status` / `current_stage` / `recovery` 收敛成终态语义
- 同时新增解释字段,例如:
- `state_conflict: true`
- `state_conflict_reason: "run_state indicates generate_summaries but run-report.json already proves the workflow reached a terminal state"`
- `status_source: "run_report_reconciliation"`
- [x] 保留 `state_source=run_state`,但增加 `status_source` / `state_quality` / `state_conflict` 之类解释字段
#### 预期收益
- OpenClaw 继续按 `status` 分支时也不会卡住
- 第一时间减少“明明产物齐了却还像没跑完”的误判
- 不需要立刻动 workflow 主链路
---
### Phase 2|修 `resume_run` 的恢复起点判断
#### 目标
避免 stale state 让恢复逻辑从 `generate_summaries` 这类过早阶段重跑。
#### 建议改动点
文件:`src/summary_mcp/runtime/resume_service.py`
#### 建议动作
- [x] 在 `_resolve_resume_from_stage()` 之前/之后加入真实 artifacts 检查
- [x] 如果以下文件已存在:
- `openclaw-delivery-payload.json`
- `digest-brief.json`
- `run-report.json`
则不要再从 `generate_summaries` 或 `apply_filters` 起跑
- [x] 为 `resume_run()` 增加“恢复起点是基于 artifacts 重算还是基于 state 推断”的返回说明
- [x] 必要时增加更保守逻辑:
- `run-report.json` 已存在时,默认拒绝继续 resume,并提示“产物已完成,请先检查状态一致性”
- 补充:默认生产模式下,主链路会稳定写出 `summary-batch` / `candidate-batch`,恢复逻辑优先消费这两个 batch artifacts;若它们缺失或不稳定,才回退到更早的安全 stage 或直接拒绝恢复
- 补充:调用方可先走 `inspect_resume_plan`,只有 `recommended_action=resume` 时再调用 `resume_run`
#### 预期收益
- 降低无意义重跑和 timeout 风险
- 让 `resume_run` 更接近真正的恢复工具,而不是误重跑工具
---
### Phase 3|补 job/run 双状态解释层
#### 目标
让 `get_freshrss_pipeline_job_status()` 和 `get_run_status()` 的关系对调用方更可理解。
#### 建议改动点
文件:`src/summary_mcp/runtime/freshrss_pipeline_jobs.py`
#### 建议动作
- [x] 在 `get_freshrss_pipeline_job_status()` 中,读取 linked run 的关键产物存在性(轻量即可)
- [x] 若 job 仍显示 `run_pipeline`,但 linked run 已有 report/payload/digest 产物:
- 不仅增加解释字段,还应直接把 job 对外 `status` 收敛为终态,避免外层永远轮询
- 例如:
- `status_source: "linked_run_reconciliation"`
- `status_note: "linked run artifacts are complete; the job can be treated as completed"`
- [x] 若 `result.json` 缺失,但 linked run 已有 `run-report.json` 与 delivery 产物:
- `get_freshrss_pipeline_job_result()` 应能基于 linked run 产物合成最小结果,至少稳定返回 `run_id`
- [x] 明确文档:job status 是外层异步任务态,不等于内部 workflow 细粒度状态
#### 预期收益
- 减少“job running / run finished”口径冲突带来的误解
- 避免 OpenClaw 因 outer job stale state 卡死在轮询和 result 读取前
---
### Phase 4|把 `resume_run` 改成异步恢复 job
#### 目标
解决当前剩余的核心问题:`resume_run` 虽然恢复判定已经安全,但执行模型仍是同步 MCP 调用,长链路恢复时依然可能超时,导致 OpenClaw 编排层“看起来像又卡住了”。
#### 建议改动点
文件:
- `src/summary_mcp/runtime/resume_jobs.py`(新)
- `scripts/run_resume_job.py`(新)
- `src/summary_mcp/server.py`
- `src/summary_mcp/runtime/__init__.py`
- `src/summary_mcp/runtime/resume_service.py`
#### 建议动作
- [x] 新增最小异步恢复接口:
- `start_resume_job(run_id)`
- `get_resume_job_status(job_id)`
- `get_resume_job_result(job_id)`
- [x] job 目录固定落到:
- `outputs/freshrss/resume_jobs/<job_id>/`
- [x] 最少产物约定:
- `run-state.json`
- `input.json`
- `result.json`(成功时)
- `job-report.json`
- [x] `start_resume_job` 内部先调用 `inspect_resume_plan`
- 只有 `recommended_action=resume` 才允许真正启动
- `read_terminal_result` / `start_new_run` 要直接在 job 输入校验阶段返回,不进入执行器
- [x] 后台执行时复用现有 `_resume_freshrss_run(...)`
- 不重写恢复业务逻辑
- 只把同步入口拆成异步 job 外壳
- [x] `resume_run(run_id)` 保留,但降级为 debug / fallback
- 文档中明确:OpenClaw 编排默认应走 resume async job,而不是同步 `resume_run`
- [x] job result 里至少稳定返回:
- `run_id`
- `resume_from_stage`
- `status`
- `result_source`
- `delivery_output` / `report_output`(若存在)
#### 预期收益
- 彻底切掉恢复阶段的 MCP 同步超时风险
- 让 OpenClaw 对“启动恢复 / 轮询恢复 / 读取恢复结果”的控制面与主 pipeline async job 保持一致
- 把“恢复判定”与“恢复执行”分层,减少误调用和卡住错觉
---
## 不建议现在就做的事
- [ ] **不要先做自动 fallback 修状态**
- 例如:看到 artifacts 齐了就直接把 run-state 强行改成 success
- 原因:这会掩盖真正的状态写回问题
- [ ] **不要先大改 workflow 主链路**
- 当前更像查询层与恢复层的状态解释缺陷
- 先修读取与恢复判断,收益更大、风险更低
---
## 建议执行顺序
1. **先改 `query_service.py`**
- 让 `get_run_status()` 能暴露 stale state / artifact conflict
2. **再改 `resume_service.py`**
- 避免从错误阶段重跑
3. **最后看 `freshrss_pipeline_jobs.py`**
- 给 job status 加 linked run 补充说明
4. **收尾改 `resume async job`**
- 让恢复执行也走正式异步控制面,避免同步恢复再把编排卡住
---
## 验收标准
### 验收 1:状态冲突识别
构造一个场景:
- `run-state.json` 留在 `generate_summaries`
- 但 payload / digest brief / run-report 已存在
期望:
- `get_run_status()` 不再只回“卡在 generate_summaries”
- 会显式返回冲突提示字段
### 验收 2:恢复起点修正
构造一个场景:
- `run-state` 指向 `generate_summaries`
- 但 `delivery_payload` / `run-report` 已存在
期望:
- `resume_run()` 不应再从 summary 阶段重跑
- 至少应拒绝恢复并提示“产物已完成,优先检查状态一致性”
### 验收 3:job/run 双层说明
构造一个场景:
- job status 仍在 `run_pipeline`
- linked run 已有关键产物
期望:
- `get_freshrss_pipeline_job_status()` 能返回补充说明,不再只有生硬 running
### 验收 4:恢复执行不再阻塞编排
构造一个场景:
- run 可恢复
- 恢复点为 `generate_summaries` 或 `apply_filters`
- 恢复执行耗时超过单次 MCP 同步窗口
期望:
- OpenClaw 调用的是 `start_resume_job(...)`,而不是同步 `resume_run(...)`
- `get_resume_job_status(job_id)` 可稳定轮询到终态
- `get_resume_job_result(job_id)` 至少稳定返回 `run_id`、`resume_from_stage` 与最终产物引用
- 即使恢复失败,也能在 job-report / result 中看清失败点,而不是只表现为调用超时
---
## 备注
截至 2026-04-14,本文件中的 Phase 1 / 2 / 3 / 4 已完成主要落地;当前 `resume` 链路已经从“状态收敛 + 安全恢复点判定”进一步补齐到“正式异步恢复执行”。