feat(reader): add async article summary jobs
This commit is contained in:
@@ -0,0 +1,757 @@
|
||||
# 单篇总结异步 job 最小版落地方案
|
||||
|
||||
## 1. 背景与问题定义
|
||||
|
||||
当前 reader 已经把日更 FreshRSS 主流程做成了带 `run-state.json` 的 runtime 模型:
|
||||
|
||||
- `src/summary_mcp/runtime/state_models.py`
|
||||
- `src/summary_mcp/runtime/run_store.py`
|
||||
- `src/summary_mcp/runtime/query_service.py`
|
||||
- `src/summary_mcp/workflows/freshrss_pipeline.py`
|
||||
|
||||
这条主链路已经具备:
|
||||
|
||||
- run / stage / artifact 的结构化状态
|
||||
- MCP 查询接口:`get_run_status` / `list_runs` / `list_run_artifacts`
|
||||
- 结果读取接口:`get_delivery_payload` / `get_run_report`
|
||||
|
||||
但“单篇总结”这条线目前还是同步调用:
|
||||
|
||||
- 核心逻辑:`src/summary_mcp/workflows/article_summary.py`
|
||||
- MCP 暴露:`src/summary_mcp/server.py` 中的 `generate_article_summaries`
|
||||
- CLI 辅助:`scripts/run_article_summaries.py`
|
||||
|
||||
现状问题已经很明确:
|
||||
|
||||
- 在 OpenClaw → MCP tool 这条链路里,`generate_article_summaries` 可能因为 tool 调用时长而 timeout
|
||||
- 但 reader 项目本体在 `.venv` 下直接跑 article summary,大约 37.5 秒即可成功
|
||||
- 这说明问题不一定在 summary 业务本身,而更可能在“同步工具调用 + 上层等待模型”这个包装层
|
||||
|
||||
所以目标不是先继续调 timeout,而是把单篇总结也纳入 **真正异步、可轮询、可落盘、可恢复基本状态** 的最小 job 模型里。
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么同步 MCP 不适合这一步
|
||||
|
||||
`generate_article_summaries` 当前在 `server.py` 里直接同步执行 `summarize_selected_articles(...)`,调用方必须一直阻塞等待,直到:
|
||||
|
||||
1. 读取 extracted payload
|
||||
2. 调用 LLM 生成总结
|
||||
3. 可选 repair retry
|
||||
4. 渲染 Markdown
|
||||
5. 写文件完成
|
||||
6. MCP tool 返回生成路径
|
||||
|
||||
这个模式对“几十秒级、依赖外部 LLM、可能重试”的任务不稳,核心问题有三层:
|
||||
|
||||
### 2.1 tool 调用时长不可控
|
||||
|
||||
`run_loop_payload()` 内部会发起外部 HTTP 请求,还可能做 repair retry。即便单次平均 37.5 秒,也已经接近很多上层编排系统的心理和技术超时边界。
|
||||
|
||||
### 2.2 调用方看不到中间状态
|
||||
|
||||
现在如果卡住,调用方只能等:
|
||||
|
||||
- 不知道是在读输入
|
||||
- 不知道是在调 LLM
|
||||
- 不知道是在重试
|
||||
- 不知道是否已经写出部分结果
|
||||
|
||||
这也是同步接口最烦的点:失败时只能看到“tool timeout / tool failed”,而不是“业务跑到哪一步了”。
|
||||
|
||||
### 2.3 与 reader 已有 runtime 风格不一致
|
||||
|
||||
FreshRSS 主流程已经是“run truth + status query + artifact read”的思路,而单篇总结仍然是黑箱同步函数。继续维持两套风格,只会让 SOP 更复杂:
|
||||
|
||||
- 日报主链路用 `run_id`
|
||||
- 单篇总结却要么同步等,要么退回 CLI fallback
|
||||
|
||||
这不利于后续把 `reader-digest-flow` 稳定成正式 SOP。
|
||||
|
||||
---
|
||||
|
||||
## 3. 本轮目标:最小真异步,不做大而全
|
||||
|
||||
这次只做 **单篇总结异步 job 最小版**,目标是:
|
||||
|
||||
> 让 OpenClaw 或其他调用方能先“启动单篇总结 job”,立即拿到 `job_id`,再通过状态接口轮询,最后读取输出文件/结果。
|
||||
|
||||
### 3.1 本轮必须做到的范围
|
||||
|
||||
1. 新增单篇总结 job 的 start/status/result 最小接口
|
||||
2. job 真正在后台执行,而不是 MCP 请求线程里阻塞到完成
|
||||
3. 状态落盘到文件,遵循 reader 当前 runtime 风格
|
||||
4. 复用现有 `summarize_selected_articles` 逻辑,不重写业务
|
||||
5. 输出仍然是现有 Markdown 文件,不改知识内容 schema
|
||||
|
||||
### 3.2 本轮明确不做
|
||||
|
||||
1. **不做通用队列系统**
|
||||
2. **不做数据库**
|
||||
3. **不做多 worker / 分布式调度**
|
||||
4. **不做取消 job / kill job**
|
||||
5. **不做并发配额控制**
|
||||
6. **不做 resume/retry from stage**
|
||||
7. **不把 article summary 一次性并入 freshrss `resume_run` 体系**
|
||||
8. **不改 summary prompt / validator / 输出格式**
|
||||
9. **不处理批量高吞吐场景优化**
|
||||
|
||||
一句话:这轮只解“同步 tool 容易 timeout,但业务本身能跑完”这个问题,不顺手扩成任务调度平台。
|
||||
|
||||
---
|
||||
|
||||
## 4. 接入当前 reader 结构的建议
|
||||
|
||||
### 4.1 复用现有 runtime 设计,但单独建 article summary job 命名空间
|
||||
|
||||
不建议把 article summary job 粗暴塞进现有 `freshrss_daily_digest` run 查询里混用一个 schema;更合适的是:
|
||||
|
||||
- 复用 `RunState / StageState / ArtifactRecord / RunStore` 这套思维
|
||||
- 但给单篇总结定义独立 workflow 名称与存储目录
|
||||
|
||||
建议:
|
||||
|
||||
- workflow: `article_summary_job`
|
||||
- run_type: `article_summary`
|
||||
- output root: `outputs/freshrss/article_summary_jobs/<job_id>/`
|
||||
|
||||
这样有几个好处:
|
||||
|
||||
- 不污染 `outputs/freshrss/rerun/`
|
||||
- 语义清楚:这是独立 job,不是假装自己是日报 rerun
|
||||
- 查询和排查时更直观
|
||||
|
||||
### 4.2 job 与结果 Markdown 解耦
|
||||
|
||||
job 目录只负责:
|
||||
|
||||
- 状态文件
|
||||
- 输入快照
|
||||
- artifact 索引
|
||||
- 执行报告
|
||||
|
||||
真正生成的总结 Markdown,仍然可以写到用户指定的 `output_dir`(或默认 `single_summaries/`)。
|
||||
|
||||
这样不破坏当前下游 SOP:
|
||||
|
||||
- `reader-digest-flow` 依然从原来的单篇总结输出目录拿 `.md`
|
||||
- job 目录只提供状态与索引,不强迫下游改结果路径约定
|
||||
|
||||
---
|
||||
|
||||
## 5. 新增工具 / API 设计
|
||||
|
||||
本轮建议新增 3 个 MCP tool,名字尽量和当前 runtime 风格一致。
|
||||
|
||||
## 5.1 `start_article_summary_job`
|
||||
|
||||
### 作用
|
||||
|
||||
启动一个后台 job,立即返回 `job_id`,不等待总结完成。
|
||||
|
||||
### 输入建议
|
||||
|
||||
```json
|
||||
{
|
||||
"extracted_path": "outputs/freshrss/rerun/<run_id>/extracted/item-01.extracted.json",
|
||||
"selected_ids": ["12345"],
|
||||
"output_dir": "outputs/freshrss/single_summaries/2026-04-10",
|
||||
"max_retries": 2,
|
||||
"timeout_seconds": 120,
|
||||
"llm_api_key": null,
|
||||
"llm_model": null,
|
||||
"llm_api_url": null
|
||||
}
|
||||
```
|
||||
|
||||
### 返回建议
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "article-summary-20260410-144500-ab12cd34",
|
||||
"workflow": "article_summary_job",
|
||||
"run_type": "article_summary",
|
||||
"status": "running",
|
||||
"output_dir": "outputs/freshrss/article_summary_jobs/article-summary-20260410-144500-ab12cd34",
|
||||
"message": "Article summary job started successfully. Use get_article_summary_job_status to poll progress."
|
||||
}
|
||||
```
|
||||
|
||||
### 约束建议
|
||||
|
||||
- `selected_ids` 第一版允许多个,但建议由 OpenClaw 每次只传一篇或少量篇,避免一个 job 干太多事
|
||||
- `extracted_path` 必须存在,否则直接拒绝启动
|
||||
- `output_dir` 不传则按当前默认逻辑推导
|
||||
|
||||
---
|
||||
|
||||
## 5.2 `get_article_summary_job_status`
|
||||
|
||||
### 作用
|
||||
|
||||
查询 job 当前状态、阶段、进度、错误摘要、已注册 artifact。
|
||||
|
||||
### 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "article-summary-20260410-144500-ab12cd34"
|
||||
}
|
||||
```
|
||||
|
||||
### 返回建议
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "article-summary-20260410-144500-ab12cd34",
|
||||
"workflow": "article_summary_job",
|
||||
"run_type": "article_summary",
|
||||
"status": "running",
|
||||
"current_stage": "generate_markdown",
|
||||
"started_at": "2026-04-10T14:45:00+08:00",
|
||||
"updated_at": "2026-04-10T14:45:23+08:00",
|
||||
"finished_at": null,
|
||||
"output_dir": "outputs/freshrss/article_summary_jobs/article-summary-20260410-144500-ab12cd34",
|
||||
"progress": {
|
||||
"completed_stage_count": 2,
|
||||
"running_stage_count": 1,
|
||||
"failed_stage_count": 0,
|
||||
"pending_stage_count": 1,
|
||||
"total_stage_count": 4
|
||||
},
|
||||
"artifacts": [...],
|
||||
"error_summary": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.3 `get_article_summary_job_result`
|
||||
|
||||
### 作用
|
||||
|
||||
当 job 成功后,返回结构化结果,供 OpenClaw 继续下游 IMA 沉淀。
|
||||
|
||||
### 输入
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "article-summary-20260410-144500-ab12cd34"
|
||||
}
|
||||
```
|
||||
|
||||
### 返回建议
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "article-summary-20260410-144500-ab12cd34",
|
||||
"status": "success",
|
||||
"written_paths": [
|
||||
"outputs/freshrss/single_summaries/2026-04-10/某篇文章标题.md"
|
||||
],
|
||||
"artifact": {
|
||||
"name": "job_result",
|
||||
"path": "outputs/freshrss/article_summary_jobs/article-summary-20260410-144500-ab12cd34/result.json",
|
||||
"kind": "json",
|
||||
"stage": "write_result"
|
||||
},
|
||||
"result": {
|
||||
"selected_ids": ["12345"],
|
||||
"written_paths": [
|
||||
"outputs/freshrss/single_summaries/2026-04-10/某篇文章标题.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 行为建议
|
||||
|
||||
- 若 job 还没完成,返回当前状态 + 提示“not ready”
|
||||
- 若 job 失败,返回失败摘要,不硬抛文件不存在异常
|
||||
|
||||
---
|
||||
|
||||
## 6. job 状态文件设计
|
||||
|
||||
建议直接复用现有 `RunState` 模型,不另造一套 schema。
|
||||
|
||||
job 目录示例:
|
||||
|
||||
```text
|
||||
outputs/freshrss/article_summary_jobs/
|
||||
article-summary-20260410-144500-ab12cd34/
|
||||
run-state.json
|
||||
input.json
|
||||
result.json
|
||||
job-report.json
|
||||
```
|
||||
|
||||
## 6.1 `run-state.json`
|
||||
|
||||
建议直接沿用当前字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "article-summary-20260410-144500-ab12cd34",
|
||||
"workflow": "article_summary_job",
|
||||
"run_type": "article_summary",
|
||||
"status": "running",
|
||||
"current_stage": "generate_markdown",
|
||||
"started_at": "2026-04-10T14:45:00+08:00",
|
||||
"updated_at": "2026-04-10T14:45:23+08:00",
|
||||
"finished_at": null,
|
||||
"input": {
|
||||
"extracted_path": "outputs/freshrss/rerun/<run_id>/extracted/item-01.extracted.json",
|
||||
"selected_ids": ["12345"],
|
||||
"output_dir": "outputs/freshrss/single_summaries/2026-04-10",
|
||||
"max_retries": 2,
|
||||
"timeout_seconds": 120
|
||||
},
|
||||
"stages": [...],
|
||||
"artifacts": [...],
|
||||
"error": null,
|
||||
"recovery": {
|
||||
"resumable": false,
|
||||
"resume_from_stage": null,
|
||||
"last_success_stage": "load_input"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第一版 stage 建议
|
||||
|
||||
建议只切 4 个 stage,够看即可:
|
||||
|
||||
1. `prepare_job`
|
||||
- 校验输入
|
||||
- 解析路径
|
||||
- 写 `input.json`
|
||||
|
||||
2. `load_input`
|
||||
- 读取 extracted payload
|
||||
- 确认 `selected_ids` 可匹配条目
|
||||
|
||||
3. `generate_markdown`
|
||||
- 调 `summarize_selected_articles(...)`
|
||||
- 这是主要耗时阶段
|
||||
|
||||
4. `write_result`
|
||||
- 写 `result.json`
|
||||
- 注册输出 artifact
|
||||
|
||||
这里不要把 LLM 调用再拆更多细 stage,否则最小版反而过度设计。
|
||||
|
||||
---
|
||||
|
||||
## 6.2 `input.json`
|
||||
|
||||
作用:保留启动请求快照,便于排查。
|
||||
|
||||
建议内容与 `run-state.input` 基本一致。
|
||||
|
||||
---
|
||||
|
||||
## 6.3 `result.json`
|
||||
|
||||
成功时写:
|
||||
|
||||
```json
|
||||
{
|
||||
"job_id": "article-summary-20260410-144500-ab12cd34",
|
||||
"selected_ids": ["12345"],
|
||||
"written_paths": [
|
||||
"outputs/freshrss/single_summaries/2026-04-10/某篇文章标题.md"
|
||||
],
|
||||
"completed_at": "2026-04-10T14:45:41+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
失败时可以不写,或只写失败快照都行。最小版建议:
|
||||
|
||||
- 成功写 `result.json`
|
||||
- 失败只依赖 `run-state.json`
|
||||
|
||||
避免双份失败状态不一致。
|
||||
|
||||
---
|
||||
|
||||
## 7. 执行模型建议:优先子进程,不建议线程
|
||||
|
||||
### 7.1 推荐:子进程后台执行
|
||||
|
||||
最小真异步推荐模型:
|
||||
|
||||
- `start_article_summary_job` 负责:
|
||||
- 创建 job 目录
|
||||
- 初始化 `run-state.json`
|
||||
- 通过 `subprocess.Popen(...)` 启动一个独立 Python 进程执行 job runner
|
||||
- 立即返回 `job_id`
|
||||
|
||||
后台 runner 再去:
|
||||
|
||||
- 读取 `input.json`
|
||||
- 用 `RunStore` 更新状态
|
||||
- 调用 `summarize_selected_articles(...)`
|
||||
- 写 `result.json`
|
||||
- finish/fail run
|
||||
|
||||
### 7.2 为什么不推荐线程
|
||||
|
||||
虽然线程实现看起来更省事,但不适合作为 reader 的正式最小异步落地:
|
||||
|
||||
1. **MCP server 进程重启后线程直接丢失**
|
||||
2. 线程状态不天然可恢复,容易出现“状态文件还在 running,但线程没了”
|
||||
3. 未来要做健康检查/孤儿 job 检测时,线程模型更难收口
|
||||
|
||||
### 7.3 为什么子进程更贴当前项目风格
|
||||
|
||||
reader 现在本来就偏“文件产物 + runtime 状态真相”风格。子进程模式有天然优势:
|
||||
|
||||
- 和 CLI/fallback 思维一致
|
||||
- 进程边界清楚
|
||||
- `run-state.json` 由实际执行者写,职责清晰
|
||||
- 将来如果要做 orphan detection / stale running job 修复,也容易补
|
||||
|
||||
### 7.4 本轮不做进程管理增强
|
||||
|
||||
最小版里,不要求:
|
||||
|
||||
- 记录 PID 后做 kill/cancel
|
||||
- 自动清理僵尸进程
|
||||
- 守护进程/worker 池
|
||||
|
||||
但建议在 `input.json` 或 `run-state.input` 里附带:
|
||||
|
||||
- `launcher_pid`
|
||||
- `runner_command`
|
||||
|
||||
方便排障。
|
||||
|
||||
---
|
||||
|
||||
## 8. 与现有 `summarize_selected_articles` 的复用关系
|
||||
|
||||
核心原则:**不重写总结业务,只包一层 job runner。**
|
||||
|
||||
### 8.1 直接复用的部分
|
||||
|
||||
`src/summary_mcp/workflows/article_summary.py` 已经做了:
|
||||
|
||||
- 读取 extracted payload
|
||||
- 根据 `selected_ids` 找条目
|
||||
- 调用 `run_loop_payload(...)`
|
||||
- 渲染 Markdown
|
||||
- 写到 `output_dir`
|
||||
- 返回 `list[Path]`
|
||||
|
||||
这些都继续用。
|
||||
|
||||
### 8.2 最小新增建议
|
||||
|
||||
建议只新增一层 runtime/service,例如:
|
||||
|
||||
- `src/summary_mcp/runtime/article_summary_jobs.py`
|
||||
|
||||
职责:
|
||||
|
||||
- 生成 `job_id`
|
||||
- 创建 job 目录
|
||||
- 初始化 `RunStore`
|
||||
- 启动 runner 子进程
|
||||
- 提供 status/result 查询
|
||||
- 在 runner 里调用 `summarize_selected_articles`
|
||||
|
||||
### 8.3 是否需要改 `summarize_selected_articles`
|
||||
|
||||
最小版尽量少改,只建议加两类低风险增强:
|
||||
|
||||
1. **可选输入校验增强**
|
||||
- 如果 `selected_ids` 一个都匹配不到,显式报错
|
||||
- 避免“成功返回空列表”却让 job 看起来像成功
|
||||
|
||||
2. **可选 hook / telemetry(非必须)**
|
||||
- 如果后面需要更细粒度写 stage 输出,可再加
|
||||
- 但第一版没必要为了观测性重构函数
|
||||
|
||||
结论:
|
||||
|
||||
- 第一版优先保持 `summarize_selected_articles` 基本不动
|
||||
- job 层只把它作为黑盒业务函数调用
|
||||
|
||||
---
|
||||
|
||||
## 9. 建议新增代码组织
|
||||
|
||||
建议新增文件:
|
||||
|
||||
```text
|
||||
src/summary_mcp/runtime/article_summary_jobs.py
|
||||
scripts/run_article_summary_job.py
|
||||
```
|
||||
|
||||
### 9.1 `article_summary_jobs.py`
|
||||
|
||||
建议包含:
|
||||
|
||||
- `start_article_summary_job(...)`
|
||||
- `run_article_summary_job(...)`
|
||||
- `get_article_summary_job_status(...)`
|
||||
- `get_article_summary_job_result(...)`
|
||||
- 若干私有 helper:
|
||||
- job id 生成
|
||||
- job dir 解析
|
||||
- result artifact 读取
|
||||
|
||||
### 9.2 `scripts/run_article_summary_job.py`
|
||||
|
||||
作用:作为子进程 runner 入口。
|
||||
|
||||
例如:
|
||||
|
||||
```bash
|
||||
python scripts/run_article_summary_job.py --job-id article-summary-...
|
||||
```
|
||||
|
||||
runner 只做一件事:
|
||||
|
||||
- 根据 `job_id` 找到 job 目录和 `input.json`
|
||||
- 真正执行 job
|
||||
|
||||
这样避免在 `Popen("python -c ...")` 里塞长字符串,也方便本地调试。
|
||||
|
||||
---
|
||||
|
||||
## 10. 对 `reader-digest-flow` SOP 的影响
|
||||
|
||||
这块是重点,因为老大的实际痛点就在这。
|
||||
|
||||
### 10.1 现状 SOP
|
||||
|
||||
当前 skill 已经有硬规则:
|
||||
|
||||
- 优先走 MCP `generate_article_summaries`
|
||||
- MCP timeout 时,立刻 fallback 到 reader 本地 `.venv`
|
||||
|
||||
这个 fallback 现在是必要的,但它本质是在补“单篇总结没有正式异步接口”。
|
||||
|
||||
### 10.2 引入异步 job 后的推荐 SOP
|
||||
|
||||
建议调整为:
|
||||
|
||||
1. OpenClaw 在用户确认保留文章后
|
||||
2. 调 `start_article_summary_job`
|
||||
3. 拿到 `job_id`
|
||||
4. 轮询 `get_article_summary_job_status`
|
||||
5. 成功后调 `get_article_summary_job_result`
|
||||
6. 再继续 IMA 格式化与上传
|
||||
|
||||
### 10.3 对 skill 文档的影响
|
||||
|
||||
`reader-digest-flow` 需要后续补一条新规则:
|
||||
|
||||
- 单篇总结的正式生产路径从“同步 MCP + 本地 fallback”升级为“异步 job + 状态轮询”
|
||||
- 本地 `.venv` CLI fallback 仍保留,但降级为:
|
||||
- job 启动失败
|
||||
- job runner 异常
|
||||
- reader 服务端出现系统性问题时的应急路径
|
||||
|
||||
### 10.4 用户体验改善
|
||||
|
||||
异步 job 后,OpenClaw 可给出更像正式系统的反馈:
|
||||
|
||||
- “已启动单篇总结任务,正在生成”
|
||||
- “当前状态:generate_markdown”
|
||||
- “已完成,准备继续沉淀到 IMA”
|
||||
|
||||
而不是现在的:
|
||||
|
||||
- 直接卡住几十秒
|
||||
- 然后 tool timeout
|
||||
- 再走一套 fallback
|
||||
|
||||
---
|
||||
|
||||
## 11. 验证方案
|
||||
|
||||
本轮验证不要追求大而全,按 4 层做就够。
|
||||
|
||||
### 11.1 单元级验证
|
||||
|
||||
目标:确保 job 状态文件和结果文件行为正确。
|
||||
|
||||
建议覆盖:
|
||||
|
||||
1. `start_article_summary_job` 能创建 job 目录与 `run-state.json`
|
||||
2. 输入路径不存在时,启动直接失败
|
||||
3. `get_article_summary_job_status` 能正确读取状态
|
||||
4. job 成功后 `get_article_summary_job_result` 返回 `written_paths`
|
||||
5. job 失败后状态为 `failed`,并带错误摘要
|
||||
|
||||
### 11.2 本地集成验证
|
||||
|
||||
用一个真实 extracted 文件跑:
|
||||
|
||||
1. 启动 job
|
||||
2. 轮询 status
|
||||
3. 成功后检查:
|
||||
- `result.json` 存在
|
||||
- Markdown 文件存在
|
||||
- 路径正确
|
||||
|
||||
### 11.3 OpenClaw 链路验证
|
||||
|
||||
在实际 `reader-digest-flow` 环节,用一篇已确认保留的文章做:
|
||||
|
||||
1. 启动 async job
|
||||
2. 等待成功
|
||||
3. 继续做 IMA markdown 整理与上传
|
||||
4. 确认整个链路不再因为同步 tool timeout 中断
|
||||
|
||||
### 11.4 异常验证
|
||||
|
||||
至少测 3 类异常:
|
||||
|
||||
1. `selected_ids` 不存在
|
||||
2. LLM 接口失败 / 超时
|
||||
3. runner 进程异常退出
|
||||
|
||||
预期:
|
||||
|
||||
- `run-state.json` 最终为 `failed`
|
||||
- `error_summary` 可读
|
||||
- 调用方能明确知道失败,而不是只看到 transport timeout
|
||||
|
||||
---
|
||||
|
||||
## 12. 风险与回滚方案
|
||||
|
||||
## 12.1 风险
|
||||
|
||||
### 风险 1:后台子进程成功启动,但状态长期卡在 running
|
||||
|
||||
常见原因:
|
||||
|
||||
- runner 进程崩了
|
||||
- server 重启时某些路径没写全
|
||||
- 子进程命令不对
|
||||
|
||||
缓解:
|
||||
|
||||
- runner 启动前就先写 `run-state.json`
|
||||
- runner 一进来先更新 `prepare_job` / `load_input`
|
||||
- 后续可加“stale running 超时判定”,但第一版先不做自动修复
|
||||
|
||||
### 风险 2:复用旧函数导致“空输出也算成功”
|
||||
|
||||
当前 `summarize_selected_articles()` 如果没匹配到文章或全部失败,存在返回空列表的可能。
|
||||
|
||||
缓解:
|
||||
|
||||
- job runner 里把“`written_paths` 为空”视为失败
|
||||
- 或者顺手在 `summarize_selected_articles` 里补显式校验
|
||||
|
||||
### 风险 3:同一时刻大量 job 并发,LLM 调用被打爆
|
||||
|
||||
第一版不解决系统级并发控制。
|
||||
|
||||
缓解:
|
||||
|
||||
- SOP 层先按单篇/少量串行用
|
||||
- skill 层避免一口气启动很多 job
|
||||
|
||||
### 风险 4:状态目录与结果目录分离,排查时容易迷路
|
||||
|
||||
缓解:
|
||||
|
||||
- 在 `result.json` 和 artifact 元数据里明确记录 `written_paths`
|
||||
- 在 `run-state.input.output_dir` 中保留结果目录
|
||||
|
||||
---
|
||||
|
||||
## 12.2 回滚方案
|
||||
|
||||
这个方案很好回滚,因为是“新增,不替换”。
|
||||
|
||||
### 回滚原则
|
||||
|
||||
- 保留现有 `generate_article_summaries`
|
||||
- 保留现有 `scripts/run_article_summaries.py`
|
||||
- 新增 async job 接口如果不稳定,直接停止在 OpenClaw 层使用即可
|
||||
|
||||
### 回滚路径
|
||||
|
||||
1. 停止调用 `start_article_summary_job`
|
||||
2. 恢复回原 SOP:
|
||||
- 先尝试同步 MCP `generate_article_summaries`
|
||||
- 失败则本地 `.venv` fallback
|
||||
3. async job 相关代码保留但不作为正式入口
|
||||
|
||||
也就是说,这轮改动不会堵死当前生产路径,风险可控。
|
||||
|
||||
---
|
||||
|
||||
## 13. 实施顺序(建议按这个顺序落地)
|
||||
|
||||
### Phase A:先把方案落地成最小代码骨架
|
||||
|
||||
1. 新增 `src/summary_mcp/runtime/article_summary_jobs.py`
|
||||
2. 新增 job 目录常量与 helper
|
||||
3. 新增 `scripts/run_article_summary_job.py`
|
||||
4. 实现 runner 内部对 `summarize_selected_articles` 的调用
|
||||
5. 先本地命令验证 job 可跑通
|
||||
|
||||
### Phase B:再把 MCP 接口接上
|
||||
|
||||
6. 在 `server.py` 新增:
|
||||
- `start_article_summary_job`
|
||||
- `get_article_summary_job_status`
|
||||
- `get_article_summary_job_result`
|
||||
7. 本地 MCP 调用验证
|
||||
|
||||
### Phase C:最后接 OpenClaw SOP
|
||||
|
||||
8. 更新 `reader-digest-flow` skill,把正式路径切到 async job
|
||||
9. 保留本地 `.venv` fallback 作为应急方案
|
||||
10. 跑一次真实日报保留文章沉淀闭环
|
||||
|
||||
---
|
||||
|
||||
## 14. 推荐的最小返回/状态语义
|
||||
|
||||
为了跟现有 runtime 风格一致,建议继续用:
|
||||
|
||||
- `status`: `running` / `success` / `failed`
|
||||
- `current_stage`: 当前 stage 名
|
||||
- `artifacts`: 注册产物列表
|
||||
- `error_summary`: 结构化错误
|
||||
|
||||
第一版不必引入:
|
||||
|
||||
- `queued`
|
||||
- `cancelled`
|
||||
- `retrying`
|
||||
- `paused`
|
||||
|
||||
避免状态机一开始就复杂化。
|
||||
|
||||
---
|
||||
|
||||
## 15. 结论 / 拍板建议
|
||||
|
||||
拍板建议很直接:
|
||||
|
||||
1. **这件事值得做,而且优先级高**,因为它正好卡在当前日报 SOP 的真实痛点上
|
||||
2. **不建议继续先修同步 timeout**,因为同步模型本身就不适合几十秒级、外部 LLM 驱动的任务
|
||||
3. **最小真异步应采用“文件状态 + 后台子进程 + start/status/result 三接口”**
|
||||
4. **业务层严格复用 `summarize_selected_articles`**,不要为异步化重写 summary 核心逻辑
|
||||
5. **先把单篇总结 job 做成独立小 runtime 命名空间**,不要急着并入 freshrss 主 run 的 resume 体系
|
||||
|
||||
如果只允许做一轮最小落地,我建议就做到:
|
||||
|
||||
- `start_article_summary_job`
|
||||
- `get_article_summary_job_status`
|
||||
- `get_article_summary_job_result`
|
||||
- `outputs/freshrss/article_summary_jobs/<job_id>/run-state.json`
|
||||
- 子进程 runner
|
||||
|
||||
这套已经足够把当前 OpenClaw timeout 问题从“同步等待”改成“正式异步轮询”,并且几乎不碰无关模块。
|
||||
Reference in New Issue
Block a user