Compare commits

...
5 Commits
Author SHA1 Message Date
root 8e27cca166 docs: sync reader-digest-flow skill with Hermes version (absolute path, extract_failed handling, rerun include_read) 2026-08-03 09:55:19 +08:00
zhuyongxin 807027976e docs: update digest template and localize references 2026-07-29 10:20:20 +08:00
zhuyongxin 6dd8cef347 refactor: simplify reader digest skill 2026-07-28 19:14:09 +08:00
root 5eb390e3ed docs: 添加 Agent Skill 到项目仓库
- 复制 reader-digest-flow skill 到 skills/ 目录(含 SKILL.md + references/)
- README 新增 Agent Skill 章节说明供 Agent 使用的工作流
2026-07-28 18:38:54 +08:00
root 7b791ac947 docs: 重写 README 为项目介绍
从 MCP 集成技术文档改为完整项目 README,包含:
- 项目定位与核心能力
- 架构概览(流程图 + MCP 工具层 + CLI)
- 快速开始与环境变量
- 项目结构一览
- 关键技术决策说明
- 关键词治理流程
2026-07-28 16:26:34 +08:00
11 changed files with 968 additions and 363 deletions
+187 -363
View File
@@ -1,390 +1,214 @@
# Reader MCP Workflow Service
# Reader · AI 日报引擎
reader 当前已经收口为面向 OpenClaw 的 MCP workflow service。正式能力边界以 FreshRSS 日报工作流为准:启动 run、写入 `run-state.json`、查询运行状态、读取结构化结果,以及异步恢复 job。CLI 与同步入口仍保留,但定位为 debug / fallback,而不是正式集成入口。
> 从 FreshRSS 到 AI 日报的自动化流水线,为个人知识管理生成每日 AI 工程化简报。
## 运行
Reader 是一个端到端的 AI 日报生产系统,定时从自建 FreshRSS 的 RSS 订阅源拉取文章,经过内容提取、LLM 筛选与摘要、关键词索引构建,最终产出两个输出:
1. **公开日报** — 推送到 [Hugo 站点](https://osiman.site/daily/) 的精选技术简报
2. **知识沉淀** — 单篇结构化摘要上传到 IMA 知识库(`daily` KB)
整个流程由 OpenClaw 编排,作为 MCP Workflow Service 对外暴露。
---
## ✨ 核心能力
| 能力 | 说明 |
|:----|:------|
| **RSS 拉取** | 从 FreshRSS API 拉取订阅文章,支持增量读取与已读标记 |
| **内容提取** | 自动提取文章正文、标题、来源等结构化字段 |
| **LLM 筛选** | 基于个人兴趣画像(`filter_context.personal.json`)自动评估文章质量,分为 keep / review / drop 三档 |
| **LLM 摘要** | 并行生成每篇文章的结构化摘要(4 路并发,约 24 秒完成 7 篇) |
| **关键词索引** | 自动构建每日关键词索引,支持别名映射与停用词过滤 |
| **候选简报** | 生成 `digest-brief.json` 供编排层(OpenClaw)决策 |
| **单篇沉淀** | 对选中的文章生成结构化知识笔记,上传到 IMA 知识库 |
| **异步 Job** | 全部生产流程走异步 job,支持恢复与状态查询 |
---
## 🏗 架构概览
```
FreshRSS ──→ 拉取 ──→ 内容提取 ──→ LLM 筛选 ──→ 关键词索引
│
digest-brief.json
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Hugo 日报 IMA 知识库 term_index
(公开简报) (单篇沉淀) (关键词数据)
```
### MCP 工具层
Reader 通过 Hermes MCP 暴露 20+ 个工具,分为三类:
**日报流水线:**
- `start_freshrss_pipeline_job` → 启动异步日报 Job
- `get_freshrss_pipeline_job_status` / `get_freshrss_pipeline_job_result` → 轮询结果
**状态查询:**
- `get_run_status` / `get_delivery_payload` / `get_run_report` → 读取运行结果
- `list_runs` / `list_run_artifacts` → 浏览运行历史
**恢复与单篇总结:**
- `inspect_resume_plan` / `start_resume_job` → 恢复失败 Job
- `start_article_summary_job` / `generate_article_summaries` → 单篇文章摘要
### CLI 入口
同步入口,适合本地 debug / fallback:
```bash
# 完整日报流水线
python scripts/run_freshrss_pipeline.py --limit 7 --mark-read --timeout 300
# 单篇文章摘要
python scripts/run_article_summaries.py \
--extracted outputs/freshrss/rerun/<run_id>/extracted/item-01.extracted.json \
--output-dir outputs/freshrss/single_summaries/YYYY-MM-DD
# 关键词维护
python scripts/build_keyword_index.py
python scripts/generate_term_cleanup_suggestions.py
```
---
## 🚀 快速开始
### 环境变量
```
# FreshRSS
FRESHRSS_API_BASE_URL=http://127.0.0.1:8081/api/greader.php
FRESHRSS_USERNAME=bot
FRESHRSS_API_PASSWORD=xxx
# LLM(主流水线)
LLM_API_URL=https://api.deepseek.com
LLM_API_KEY=xxx
LLM_MODEL=deepseek-chat
# LLM(可选,单篇摘要独立模型)
ARTICLE_SUMMARY_LLM_API_URL=https://api.deepseek.com
ARTICLE_SUMMARY_LLM_API_KEY=xxx
ARTICLE_SUMMARY_LLM_MODEL=deepseek-chat
# IMA 知识库(可选,仅沉淀时需要)
IMA_DAILY_KNOWLEDGE_BASE_ID=xxx
IMA_DAILY_KNOWLEDGE_BASE_NAME=daily
```
### 运行
```bash
# 安装
pip install -e .
# 跑日报流水线(CLI 模式)
python scripts/run_freshrss_pipeline.py --limit 7 --mark-read --timeout 300
# 启动 MCP 服务(OpenClaw 集成用)
summary-mcp
```
服务当前暴露 21 个工具。
---
正式集成摘要:
## 📁 项目结构
- 主日报正式入口:`start_freshrss_pipeline_job`
- 主日报正式读取:`get_run_status`、`get_delivery_payload`、`get_run_report`
- 恢复正式入口:`inspect_resume_plan`、`start_resume_job`、`get_resume_job_status`、`get_resume_job_result`
- 单篇总结正式入口:`start_article_summary_job`、`get_article_summary_job_status`、`get_article_summary_job_result`
- `run_freshrss_openclaw_pipeline`、`resume_run`、`generate_article_summaries` 仅用于同步 debug / fallback
## 文档入口
如果你在做 OpenClaw 集成,不要只看这个 README,优先看:
- `docs/openclaw/README.md`
- `docs/openclaw/openclaw-handoff.md`
- `docs/openclaw/openclaw-orchestration-flow.md`
字段契约见:
- `docs/openclaw/openclaw-candidate-input-field-spec.md`
- `docs/openclaw/openclaw-delivery-payload-spec.md`
文档总索引见:
- `docs/README.md`
- `docs/current/context-reset-brief.md`
- `docs/design/README.md`
- `plans/README.md`
## 正式能力边界
- 当前正式 workflow 只有 `freshrss_daily_digest`
- 当前生产编排默认走异步 job,而不是同步 MCP / CLI
- 每次 FreshRSS 主流水线 run 都会在 `outputs/freshrss/rerun/<run_dir>/run-state.json` 落地运行真相
- OpenClaw 正式读取结果应优先使用 MCP 返回的 `run_id`、`output_dir`、`delivery_output`、`report_output`
- 正式恢复只支持带有效 `run-state.json` 的当前 run,不处理历史推断 run
- 正式生产恢复依赖 `summary/summary-batch.json` 与 `candidates/candidate-batch.json`
## OpenClaw 最短调用路径
1. 调 `start_freshrss_pipeline_job`
2. 轮询 `get_freshrss_pipeline_job_status`
3. 成功后读 `get_freshrss_pipeline_job_result`,拿 `run_id`
4. 用 `get_run_status`、`get_delivery_payload`、`get_run_report` 做后续读取
5. 如需恢复,先调 `inspect_resume_plan`,只有 `recommended_action=resume` 才走 `start_resume_job`
更完整的状态分支、恢复策略和人工介入条件见 `docs/openclaw/openclaw-orchestration-flow.md`。
## 单篇文章总结后处理(可选使用独立 LLM)
### 生产环境推荐输入
- 单篇总结的正式生产输入,优先使用 FreshRSS 主流水线输出的**单篇 extracted 文件**:
- `outputs/freshrss/rerun/<run_id>/extracted/item-XX.extracted.json`
- 这些**逐条 extracted 文件**是下游单篇总结的**正式默认产物**。
- 像 `outputs/freshrss/extracted/freshrss.extracted.json` 这样的**批量 extracted 文件**,只作为临时场景、兼容旧流程的输入形态保留,**不是首选生产默认**。
### daily 知识库默认配置
- `IMA_DAILY_KNOWLEDGE_BASE_ID` —— 单篇日报总结默认上传的 IMA 知识库 ID
- `IMA_DAILY_KNOWLEDGE_BASE_NAME` —— 默认知识库名称(预期值:`daily`)
- 上传逻辑在运行时应先校验目标知识库;若配置的目标不存在,应先按名称查找,仍不存在则创建 `daily`
相关能力:
- 正式路径:`start_article_summary_job` / `get_article_summary_job_status` / `get_article_summary_job_result`
- 同步 debug:`generate_article_summaries`
- CLI:`scripts/run_article_summaries.py`
- 后台 runner:`scripts/run_article_summary_job.py`
## 校验 LLM 摘要结果
```bash
validate-llm-result outputs/reference/summary/result.json --extracted outputs/reference/extracted/read-flow-2026.extracted.json
```
reader/
├── configs/ # 配置
│ ├── filter_context.personal.json # 个人兴趣画像
│ ├── term_aliases.json # 关键词别名映射(149 条)
│ ├── term_stopwords.json # 关键词停用词(132 条)
│ └── term_cleanup_policy.json # 关键词清理策略
├── src/
│ └── summary_mcp/ # MCP 服务核心
│ ├── server.py # MCP 服务入口
│ ├── runtime/ # 运行时(Job 管理、状态持久化)
│ └── workflows/ # 工作流(日报流水线逻辑)
├── scripts/ # CLI 入口
├── outputs/ # 运行时产出
│ └── freshrss/
│ ├── rerun/<run_id>/ # 每次运行的全量产物
│ │ ├── candidates/ # digest-brief.json, delivery payload
│ │ ├── extracted/ # item-XX.extracted.json
│ │ └── run-state.json # 运行状态
│ └── single_summaries/ # 单篇摘要输出
├── data/
│ └── term_index/ # 关键词索引数据
│ ├── daily/YYYY-MM-DD.json
│ └── term_stats.json
├── docs/ # 设计文档
└── prompts/ # LLM Prompt 模板
```
## 跑最小 extraction → summary 循环
---
## ⚙️ 关键技术决策
| 决策 | 选择 | 原因 |
|:----|:----|:------|
| 运行模式 | **异步 Job** 为主,CLI fallback | 避免 MCP 传输层 120s 超时限制 |
| 摘要并发 | **ThreadPoolExecutor(max_workers=4)** | LLM 调用是 I/O 密集型,4 路并行将 7 篇摘要从 2-3 分钟压到 ~24 秒 |
| 环境变量 | **子进程显式注入 .env** | 解决 MCP 服务器环境隔离导致子进程读取不到 LLM_API_KEY 的问题 |
| 关键词过滤 | **别名映射 + 停用词 + 语义清洗** | 先用 `term_aliases.json` 归一化,再用 `term_stopwords.json` 过滤噪声,最后通过 LLM 做语义级清洗 |
| Tag 选择 | **复用已有通用 Tag**,不从 term_index 翻生僻词 | 保持 Hugo 站点 /tags/ 页面整洁,避免大量一次性专有名词 |
---
## 🔧 关键词治理
配置治理走四步流程(`scripts/` 下脚本):
```bash
python scripts/run_summary_loop.py ^
--extracted outputs/reference/extracted/read-flow-2026.extracted.json ^
--prompt outputs/prompts/llm-summary-prompt.txt ^
--output outputs/reference/summary/result.loop.json
# 1. 构建评审数据包
python skills/keyword-cleanup-review/scripts/build_review_bundle.py --days 7 --top 50
# 2. 统计规则级建议(大小写、单复数、频次阈值)
python scripts/generate_term_cleanup_suggestions.py
# 3. LLM 语义级建议(中英映射、简称-全称、近义词)
python scripts/generate_term_cleanup_semantic_suggestions.py
# 4. 确认后写入配置
python scripts/apply_term_suggestions.py --accept-watch ... --dry-run
```
## 拉取 FreshRSS 条目并映射为标准化 `item`
详见 `docs/design/keyword-engine-maintenance.md`。
```bash
set FRESHRSS_API_BASE_URL=http://127.0.0.1:8081/api/greader.php
set FRESHRSS_USERNAME=bot
set FRESHRSS_API_PASSWORD=your-api-password
python scripts/pull_freshrss_items.py --limit 5 --mark-read
```
---
默认会排除已经带 `read` 标签的条目。
如果你想拿到完整阅读列表,可以加 `--include-read`。
启用 `--mark-read` 后,脚本会在执行成功后把本次抓到的条目标记为已读。
## 🤖 Agent Skill
脚本会写出:
Reader 附带一个完整的 OpenClaw Agent Skill,位于 `skills/reader-digest-flow/`,供 AI Agent(Hermes / Claude Code 等)编排每日日报流程使用。
- `outputs/freshrss/raw/freshrss.raw.json`
- `outputs/freshrss/items/freshrss.items.json`
Skill 包含完整的 7 阶段工作流定义:
1. **Phase 1** — 跑 Pipeline(FreshRSS → 提取 → LLM 筛选)
2. **Phase 2** — 汇报候选(展示候选文章给用户决策)
3. **Phase 3** — 用户选文(选择 Hugo 发布文章)
4. **Phase 4** — 生成并发布 Hugo 日报
5. **Phase 5** — 用户选 IMA 沉淀文章
6. **Phase 6** — LLM 摘要生成
7. **Phase 7** — IMA 知识库上传
## 拉取 FreshRSS 条目并逐条做内容提取
以及海量铁律(不重跑 pipeline、编号规则、Tag 选择规范、IMA 上传流程等)和参考文件(`references/` 目录)。
```bash
set FRESHRSS_API_BASE_URL=http://127.0.0.1:8081/api/greader.php
set FRESHRSS_USERNAME=osiman
set FRESHRSS_API_PASSWORD=your-api-password
python scripts/run_freshrss_extract.py --limit 1 --mark-read
```
---
默认会排除已经带 `read` 标签的条目。
启用 `--mark-read` 后,只有提取成功的条目才会被标记为已读。
## 📄 文档
脚本会写出:
- `docs/openclaw/README.md` — OpenClaw 集成指南
- `docs/openclaw/openclaw-orchestration-flow.md` — 编排流程
- `docs/openclaw/openclaw-delivery-payload-spec.md` — 字段契约
- `docs/design/README.md` — 设计文档总索引
- `docs/design/filter-rule-engine-design.md` — 过滤规则引擎设计
- `docs/design/filter-rule-engine-usage.md` — 过滤规则使用说明
- `outputs/freshrss/raw/freshrss.raw.json`
- `outputs/freshrss/items/freshrss.items.json`
- `outputs/freshrss/extracted/freshrss.extracted.json`
---
注意:这个**批量 extracted 文件**主要用于独立提取场景和旧流程兼容。下游单篇总结的正式生产默认输入,仍然是 `outputs/freshrss/rerun/<run_id>/extracted/item-XX.extracted.json` 这类**逐条 extracted 文件**。
## 📝 License
## 跑完整 FreshRSS 流水线,并在最终 delivery payload 写盘成功后再标记已读
```bash
set FRESHRSS_API_BASE_URL=http://127.0.0.1:8081/api/greader.php
set FRESHRSS_USERNAME=osiman
set FRESHRSS_API_PASSWORD=your-api-password
set LLM_API_URL=https://api.deepseek.com
set LLM_API_KEY=your-llm-api-key
set LLM_MODEL=deepseek-chat
python scripts/run_freshrss_pipeline.py --limit 5 --mark-read
```
如果你希望过滤时引入个人工程兴趣 / AI Agent 兴趣画像,可以传入 context 文件:
```bash
python scripts/run_freshrss_pipeline.py ^
--limit 5 ^
--context configs/filter_context.personal.json ^
--mark-read
```
这条 CLI 与 MCP `run_freshrss_openclaw_pipeline` / `start_freshrss_pipeline_job` 共用同一条主流水线逻辑,但正式生产集成应优先走 async MCP job;CLI 与同步 MCP 入口仅用于本地 debug / fallback。默认会写出这些产物:
- `outputs/freshrss/rerun/<run_dir>/run-state.json`
- `outputs/freshrss/rerun/<run_dir>/raw/freshrss.raw.json`
- `outputs/freshrss/rerun/<run_dir>/candidates/openclaw-delivery-payload.json`
- `outputs/freshrss/rerun/<run_dir>/candidates/digest-brief.json`(给 OpenClaw 生成 public digest 用的轻量输入,仅包含 `keep` 候选)
- `outputs/freshrss/rerun/<run_dir>/run-report.json`
- `outputs/freshrss/rerun/<run_dir>/extracted/item-XX.extracted.json`(每篇一份)
同时还会更新每日关键词索引运行数据:
- `data/term_index/daily/YYYY-MM-DD.json`
- `data/term_index/term_stats.json`
主流水线默认**不会**产出批量级的 `freshrss.extracted.json`。
如果你需要更多逐条中间产物,例如标准化 items、摘要结果、过滤决策、candidate record、candidate input,可以加 `--debug-artifacts`。
当 OpenClaw 接入这个 MCP 服务后,应先通过 `start_freshrss_pipeline_job` 启动任务,轮询 `get_freshrss_pipeline_job_status`,再从 `get_freshrss_pipeline_job_result` 读取稳定的 `run_id`。拿到 `run_id` 之后,再通过 `get_run_status` / `get_delivery_payload` / `get_run_report` 读取状态与结果,而不是直接拼接目录路径。`run_freshrss_openclaw_pipeline` 仅保留为同步 debug / fallback 路径。
在排查复杂问题时,也可以把 `debug_artifacts=true` 打开,并结合 `list_run_artifacts` 查看该 run 下实际产物。
## 对结构化摘要结果执行确定性过滤规则
```bash
python scripts/run_filter_rules.py ^
--summary outputs/reference/summary/result.loop.json ^
--extracted outputs/reference/extracted/read-flow-2026.extracted.json ^
--output outputs/reference/filter/filter-decision.json
```
如果你希望注入兴趣主题或来源标签,也可以额外传入 context 文件:
```bash
python scripts/run_filter_rules.py ^
--summary outputs/reference/summary/result.loop.json ^
--extracted outputs/reference/extracted/read-flow-2026.extracted.json ^
--context outputs/reference/filter/filter-context.json ^
--output outputs/reference/filter/filter-decision.with-context.json
```
规则引擎设计和规则编写说明见:
- `docs/design/filter-rule-engine-design.md`
- `docs/design/filter-rule-engine-usage.md`
## 将过滤结果写入 Markdown sink
```bash
python scripts/run_markdown_sink.py ^
--summary outputs/reference/summary/result.loop.json ^
--extracted outputs/reference/extracted/read-flow-2026.extracted.json ^
--filter outputs/reference/filter/filter-decision.json
```
脚本会把 Markdown 笔记写到 `knowledge-base/` 下。
## 构建内部 `ArticleCandidateRecord` 与精简版 `OpenClawCandidateInput`
```bash
python scripts/run_article_candidate.py ^
--summary outputs/reference/summary/result.loop.json ^
--extracted outputs/reference/extracted/read-flow-2026.extracted.json ^
--filter outputs/reference/filter/filter-decision.json ^
--section-hint tools_and_workflows
```
默认会写出:
- `outputs/reference/candidates/article-candidate-record.json`
- `outputs/reference/candidates/openclaw-candidate-input.json`
## 构建批量 OpenClaw delivery payload
```bash
python scripts/build_openclaw_delivery.py ^
--input-dir outputs/freshrss/candidates/batch ^
--sort-by-rank ^
--date 2026-03-25
```
默认会写出:
- `outputs/reference/candidates/openclaw-delivery-payload.json`
输出目录布局说明见 `outputs/README.md`。
## 关键词索引默认配置
相关配置文件位于:
- `configs/term_aliases.json`
- `configs/term_stopwords.json`
- `configs/term_cleanup_policy.json`
- `configs/term_watchlist.json`
- `configs/term_change_log.json`
你也可以基于已有 delivery payload 重新构建关键词索引:
```bash
python scripts/build_keyword_index.py ^
--input outputs/reference/candidates/openclaw-delivery-payload.json
```
运行期关键词数据存放在:
- `data/term_index/`
关键词清理评审 skill 位于:
- `skills/keyword-cleanup-review/`
构建给关键词治理流程使用的评审数据包(review bundle,临时工作文件):
```bash
python skills/keyword-cleanup-review/scripts/build_review_bundle.py ^
--days 7 ^
--top 50 ^
--output outputs/term_index/review/keyword-cleanup-bundle.json
```
这个评审数据包(review bundle)会额外携带治理上下文:
- 来自 `configs/term_cleanup_policy.json` 的清理阈值
- 当前 watch list(`configs/term_watchlist.json`)
- 最近已应用的变更(`configs/term_change_log.json`)
接下来可以把 bundle 渲染成正式建议产物(默认走确定性规则,不把 LLM 作为默认路径):
```bash
python scripts/generate_term_cleanup_suggestions.py ^
--bundle outputs/term_index/review/keyword-cleanup-bundle.json
```
默认只生成:
- `outputs/term_index/review/term-cleanup-suggestions-YYYY-MM-DD.json`(唯一正式建议产物,建议短期保留)
如需人工审阅展示稿,再显式加:
```bash
python scripts/generate_term_cleanup_suggestions.py ^
--bundle outputs/term_index/review/keyword-cleanup-bundle.json ^
--emit-markdown
```
这时才会额外生成:
- `outputs/term_index/review/term-cleanup-suggestions-YYYY-MM-DD.md`(临时展示稿,可按需生成,不必作为长期资产保留)
其中本轮最小版本优先覆盖 `interest_keyword_suggestions` 和 `watch_terms` 主链路;`alias_suggestions` / `stopword_suggestions` 先保持保守。
产物保留策略建议:
- `data/term_index/daily/YYYY-MM-DD.json`、`data/term_index/term_stats.json` 作为事实层长期保留
- `configs/filter_context.personal.json`、`configs/term_watchlist.json`、`configs/term_aliases.json`、`configs/term_stopwords.json`、`configs/term_change_log.json` 作为状态层长期保留
- `term-cleanup-suggestions-YYYY-MM-DD.json` 作为正式建议产物短期保留(最近少量几份或仅保留已应用过的)
- `keyword-cleanup-bundle.json` 仅作为临时工作文件,默认只保留当前最新一份
- `term-cleanup-suggestions-YYYY-MM-DD.md` 仅作为临时展示稿,优先按需生成,不建议默认长期归档
如果你想先预览已接受建议,再决定是否写配置文件:
```bash
python scripts/apply_term_suggestions.py ^
--suggestions outputs/term_index/review/term-cleanup-suggestions-YYYY-MM-DD.json ^
--accept-watch Cron Heartbeat Memory ^
--dry-run
```
去掉 `--dry-run` 后才会真正写文件。
这个脚本也支持通过 `--accept-alias`、`--accept-stopword`、`--accept-interest` 应用 alias / stopword / interest keyword 变更。
已接受的 watch 词会写入 `configs/term_watchlist.json`,每次应用动作也会被追加到 `configs/term_change_log.json`。
## 单篇总结 LLM 配置
如果你希望单篇总结后处理使用独立模型,而不影响主流水线,可以设置:
- `ARTICLE_SUMMARY_LLM_API_URL`
- `ARTICLE_SUMMARY_LLM_MODEL`
- `ARTICLE_SUMMARY_LLM_API_KEY`
如果这些变量未设置,单篇总结会回退使用主流程中的 `LLM_*` / `OPENAI_*` 配置。
如果显式指定 DeepSeek 作为单篇总结模型且请求超时或失败,当前实现会自动再用主流程默认模型配置重试一次。
示例(PowerShell 风格):
```bash
set ARTICLE_SUMMARY_LLM_API_URL=https://api.deepseek.com
set ARTICLE_SUMMARY_LLM_API_KEY=your-article-summary-key
set ARTICLE_SUMMARY_LLM_MODEL=deepseek-chat
```
然后可以这样调用 CLI。
正式生产环境建议优先使用 `outputs/freshrss/rerun/<run_id>/extracted/` 下的**逐条 extracted 文件**;下面这个**批量 extracted** 示例仅保留为兼容旧流程 / 临时场景输入:
```bash
python scripts/run_article_summaries.py ^
--extracted outputs/freshrss/extracted/freshrss.extracted.json ^
--ids 12345 67890 ^
--output-dir outputs/freshrss/single_summaries ^
--timeout 120
```
也可以通过 `summary_mcp.server` 暴露的 MCP 工具 `generate_article_summaries` 调用:
- `extracted_path`(string):单篇 extracted JSON 路径(例如 `outputs/freshrss/rerun/<run_id>/extracted/item-01.extracted.json`),或者包含 `results` 数组的 batch extracted JSON
- `selected_ids`(array of strings):必填,至少传一个 `item_id`;如果传入的 ID 在 extracted payload 中一个都匹配不到,会直接报错
- `output_dir`(optional string):Markdown 输出目录;若不传,则默认写到 extracted 文件旁边的 `single_summaries/` 目录
- `llm_api_key` / `llm_model` / `llm_api_url`(optional strings):单篇总结 LLM 的覆盖配置;不传时会按前文规则回退到 `ARTICLE_SUMMARY_*` 或主 `LLM_*`
该工具返回一个 JSON 数组,内容为生成好的 Markdown 文件路径。
OpenClaw / 正式集成建议优先走异步 job:
- 调 `start_article_summary_job` 启动任务,立即拿到 `job_id`
- 轮询 `get_article_summary_job_status(job_id)`,直到 `status` 进入 `success` 或 `failed`
- 成功后调用 `get_article_summary_job_result(job_id)` 读取 `written_paths` 与结构化结果
- 失败时优先查看 `error_summary` 与 job 目录中的 `job-report.json`
异步 job 状态目录固定落在 `outputs/freshrss/article_summary_jobs/<job_id>/`,最小会包含:
- `run-state.json`
- `input.json`
- `result.json`(成功时)
- `job-report.json`
单篇总结使用独立 prompt:`outputs/prompts/article-summary-prompt.txt`。
它与日报 prompt 完全独立,输出的是中文结构化知识笔记,包含这些部分:
- 核心结论
- 主要论点
- 关键方法 / 机制
- 重要细节
- 可复用启发
- 关键词
- 主题
MIT
+186
View File
@@ -0,0 +1,186 @@
---
name: reader-digest-flow
description: 编排 reader 项目的端到端 AI 日报流程。仅在用户要求运行/重跑日报、汇报候选、发布 Hugo 日报、沉淀选中文章或继续已有日报任务时使用;覆盖异步 MCP 任务、候选确认、发布、单篇摘要和 IMA 知识库上传。不要因验证、排障冲动或候选质量不佳自行重跑。
---
# Reader Digest Flow
## 职责边界
本 Skill 负责:
- 通过 reader MCP 启动、观察和恢复日报任务;
- 向用户展示候选并保持稳定编号;
- 根据用户选择生成并发布 Hugo 日报;
- 对用户选中的文章生成知识笔记并编排 IMA 上传;
- 在每个副作用边界执行确认和结果验证。
本 Skill 不负责:
- 实现 reader 内部抓取、摘要、过滤或恢复逻辑;
- 通过手拼目录推导 Run 状态或 Artifact;
- 未经用户要求自行重跑 Pipeline;
- 未经用户确认发布日报或写入知识库;
- 直接维护关键词配置;关键词治理委托给 `keyword-cleanup-review`。
## 核心规则
1. **只按用户指令运行。** 只有用户明确要求“跑日报”“重新跑”“再跑一次”时才启动新 Pipeline。验证、解释排序和排障默认读取已有 Run。
2. **一次对话绑定一个当前 Run。** 以异步 Job 结果返回的 `run_id` 为稳定句柄;新 Run 产生新的候选编号体系,不混用历史编号。
3. **状态以 MCP 返回为准。** Agent 只根据顶层 `status` 和 `recommended_action` 分支;`status_source`、`state_conflict` 仅用于解释。
4. **路径以返回值为准。** 使用 `output_dir`、`artifact.path`、`delivery_output`、`report_output` 和 `written_paths`;不要根据 `run_id` 手拼 `outputs/...`。
5. **候选编号保持稳定。** 用户编号永远对应当前候选列表的原始顺序(1-based);跨产物读取详情时按 URL 或完整 `item_id` 关联,不按数组位置关联。
6. **副作用必须授权。** 用户确认 Hugo 文章后才能发布;用户确认 IMA 文章后才能生成并上传知识笔记。
7. **内容必须有来源。** 日报和知识笔记只能基于当前 Run 的 `article.plain_text`、摘要、highlights 等 Artifact;不得使用通用知识补写原文没有的信息,也不为满足长度而扩写。
## 默认生产参数
用户未显式覆盖时使用:
```json
{
"limit": 7,
"include_read": false,
"mark_read": true,
"debug_artifacts": false,
"timeout_seconds": 60,
"max_retries": 2
}
```
- 默认只处理未读文章。
- `include_read=true` 仅在用户明确要求扩大到已读内容时使用。
- debug/test/validation 才允许 `mark_read=false` 或 `debug_artifacts=true`。
- 不随机生成文章数量;用户指定 `limit` 时按用户值执行。
## 正式流程
### Phase 1:启动并观察日报 Job
正式生产入口统一为异步 MCP:
1. 调用 `start_freshrss_pipeline_job`;
2. 轮询 `get_freshrss_pipeline_job_status`;
3. `status=success` 后调用 `get_freshrss_pipeline_job_result`;
4. 保存返回的 `run_id` 和 Artifact 路径;
5. 使用 `get_run_status`、`get_delivery_payload`、`get_run_report` 读取业务状态和结果。
失败时:
1. 使用 `get_run_status(run_id)` 读取关联 Run;
2. 调用 `inspect_resume_plan(run_id)`;
3. `recommended_action=resume` 时启动并轮询异步 Resume Job;
4. `recommended_action=read_terminal_result` 时直接读取已有终态结果;
5. `recommended_action=start_new_run` 时停止并向用户报告,不自行新建 Run。
CLI 仅用于 MCP 不可用时的 fallback、debug 或人工排障,不是默认生产入口。具体调用序列见 `references/flow.md`。
### Phase 2:汇报候选
- 使用当前 Run 返回的 Delivery Payload 或 digest brief Artifact;
- 按候选原始顺序从 1 编号,状态可显示为“已入选/待确认”,但不得重新分组编号;
- 每篇提供标题、来源、2-3 句摘要和筛选理由,避免原始 JSON dump;
- 用户质疑编号或排序时读取当前 Run 产物核对,不重新运行 Pipeline;
- 需要跨 Artifact 取详情时按 URL 或完整 `item_id` 交叉验证。
Feishu 输出不要使用 Markdown 表格,见 `references/feishu-format-notes.md`。
### Phase 3:等待 Hugo 选择
- 等待用户明确选择要发布的文章;
- 用户编号映射到当前候选列表,不映射到 extracted 文件序号;
- 用户拒绝发布时立即停止当日日报后续流程,不劝说、不自动换一批;
- 用户明确要求重跑时才创建新 Run,并重新建立编号体系。
### Phase 4:生成并发布 Hugo 日报
发布前读取 `references/public-digest-example.md`,按其最终页面结构生成:
- `今日概览`
- `今日重点`
- `趋势观察`
每篇 `今日重点` 文章末尾必须添加 `来源:[来源名](原文 URL)`,来源链接跟随对应文章,不再生成独立的 `延伸阅读` 章节或重复链接。
仅发布用户在 Phase 3 选中的文章。公开页面不得出现 `keep/review/drop`、候选、待确认等内部状态。
写入 Hugo 后执行部署,并验证首页、日报列表页和当日详情页均可访问。命令和检查项见 `references/flow.md`。
### Phase 5:等待 IMA 选择
Hugo 发布选择与知识沉淀选择相互独立。询问用户哪些文章值得长期保存:
- 仅处理用户明确选择的文章;
- 不把整份日报上传到 IMA;
- 本次选择本身即授权后续单篇摘要和 IMA 上传,不重复确认。
### Phase 6:生成单篇知识笔记
对每篇选中文章:
1. 通过 URL/完整 `item_id` 找到对应 extracted Artifact;
2. 使用 `start_article_summary_job(extracted_path=<返回的 Artifact 路径>, selected_ids=[<完整 item_id>])`;
3. 轮询 `get_article_summary_job_status`,成功后读取 `get_article_summary_job_result`;
4. 只使用现有 `article.plain_text`,不重新抓取原 URL;
5. 使用返回的 `written_paths` 定位结果并检查 Markdown 内容。
6. **`extracted_path` 必须传绝对路径**(前缀 `/home/ubuntu/zhu/github/reader/`):summary-mcp 工作目录是 `/root/.hermes`,相对路径会报 `extracted_path does not exist`。同一工具连续 3 次失败会触发 MCP 冷却(约 45-60s,报 `MCP server 'reader' is unreachable`),等待冷却后再重试,不要循环重试同一调用。
异步 MCP 不可用时才使用项目 CLI fallback。不要因为内容较短而引入原文之外的知识。
### Phase 7:上传到 IMA
上传前按需读取:
- 格式规则:`references/ima-format-quickref.md`
- API 和上传步骤:`references/ima-upload-api.md`(含 `-200` 版本拦截修复)
- 凭证定位:`references/ima-credential-chain.md`
- 批量上传脚本:`scripts/ima_upload_one.py`(Python 编排,规避中文文件名 bash 引号问题)
硬规则:
- 使用完整文章标题作为文件名;
- 以 Markdown 文件 `media_type=7` 上传到 `daily` knowledge base;
- 保留原文 URL 和 Category,保证来源可追踪;
- 不使用 URL 导入或 Notes 类型替代知识库文件;
- 上传后验证目标知识库中存在对应条目;
- 失败时报告具体阶段,不无限重试。
## 关键词治理路由
只有用户明确要求“清理关键词”“词库治理”等操作时才触发关键词治理。Review bundle 和 Suggestions 生成委托给 `keyword-cleanup-review`,确认与 Apply 仍由当前编排层负责:
1. 生成 review bundle;
2. 生成规则层与可选语义层 Suggestions JSON;
3. 等待人工确认;
4. dry-run 后按精确 accept 参数 Apply。
`reader-digest-flow` 不直接编辑 `term_aliases.json`、`term_stopwords.json` 或兴趣配置。简要路由见 `references/keyword-engine-maintenance.md`。
## 环境坑位(本机部署)
- **MCP 相对路径陷阱**:`summary-mcp` 服务工作目录是 `/root/.hermes`,不是 reader 项目根。MCP 返回的 `output_dir`/`artifact.path` 是相对路径,直接传给 `start_article_summary_job(extracted_path=...)` 会报 `extracted_path does not exist`。传入前必须拼绝对路径前缀 `/home/ubuntu/zhu/github/reader/`。
- **提取失败不等于运行失败**:`status_counts.extract_failed` 的条目(`CONTENT_EXTRACTION_FAILED`,`retryable=false`)跳过即可并如实汇报;失败文章常是推广/活动等低价值内容,不因此自行重跑。`linked_run_status=partial` 时先读 run-report 的 item 级 `error` 确认原因。
- **用户要求"重新跑一批"**:候选质量低(用户主动提出)时重跑,应 `include_read=true` 并调高 `limit`(如 10),否则默认 `include_read=false` 会拉回同一批未读文章。重跑是新 Run,候选编号体系重新建立,汇报时提醒用户按新列表选择。
## 停止与人工介入
出现以下任一情况时停止自动流程并报告:
- 用户没有授权运行、发布或知识库写入;
- Job/Run 返回不可恢复,或连续恢复失败;
- Payload、候选 ID 或 Artifact 之间无法可靠关联;
- 生成内容缺少可追踪来源;
- Hugo 部署验证失败;
- IMA 凭证、目标知识库或上传结果无法验证。
## Reference 路由
- `references/flow.md`:具体 MCP 调用序列、候选映射(含 extracted_path 绝对路径、候选≠文件名顺序)、Hugo 发布和 IMA 主步骤。
- `references/content-extraction.md`:FreshRSS 内容来源与 `plain_text` 质量判断。
- `references/public-digest-example.md`:可直接参考的 Hugo 最终页面结构。
- `references/feishu-format-notes.md`:Feishu 输出格式限制。
- `references/ima-format-quickref.md`:IMA Markdown 格式规则。
- `references/ima-upload-api.md`:IMA Markdown 文件上传 API(含 `-200` 版本拦截修复)。
- `references/ima-credential-chain.md`:IMA 凭证与知识库配置定位。
- `references/keyword-engine-maintenance.md`:关键词治理 Skill 路由。
- `scripts/ima_upload_one.py`:单篇 Markdown 上传 daily 知识库的完整 Python 脚本(preflight→重名→create_media→COS→add_knowledge)。
@@ -0,0 +1,45 @@
# 内容提取流程
本文说明处理流水线如何把 FreshRSS 条目转换为可供摘要使用的文章文本。
## 核心规则:FreshRSS 条目不重新抓取原文 URL
**FreshRSS 是仅提供 RSS 内容的上游。** 对于 FreshRSS 条目,流水线不会向文章原始 URL 发起 HTTP 请求。该行为由 `pipeline.py` 中的 `RSS_ONLY_UPSTREAMS = {"freshrss"}` 强制保证。
唯一例外是非 FreshRSS 上游。未来未设置 `upstream: freshrss` 的其他来源,可以在必要时使用 `fetch_html()` 作为回退。
## 内容来源优先级
`content_loader.py` 按以下顺序检查内容,并使用第一个包含 **至少 500 个可读字符** 的来源:
| 优先级 | 来源 | 含义 |
|--------|------|------|
| 1 | `raw_html` | 通过 `ExtractionInput.raw_html` 预先注入的 HTML;常规 FreshRSS 运行中很少使用。 |
| 2 | `item.raw_content` | RSS `<content:encoded>` 中的文章正文;部分订阅源提供,部分不提供。 |
| 3 | `item.raw_summary` | RSS `<description>` 中的摘要或片段;这是当前运行中最常见的来源。 |
| 4 | `rss_content` | 来自非条目字段的独立 RSS 内容。 |
| — | `none` | 没有可用内容;FreshRSS 不允许回源抓取,因此抛出 `RSS_CONTENT_MISSING`。 |
## `content_source` 与文本质量的关系
每个 `item-XX.extracted.json` 中的 `content_source` 字段表示流水线实际使用的内容来源:
- **`item.raw_content`**:RSS `<content:encoded>` 提供的文章正文,通常质量最好,接近直接阅读原文。
- **`item.raw_summary`**:只有 RSS 摘要或描述,并非完整正文。不同来源长度差异较大,通常为 300-2000 个字符;AI 摘要基于该片段,而不是完整文章。
- **`rss_content`**:来自独立 RSS 内容,质量取决于订阅源。
- **`fetched_html`**:从原始 URL 抓取的 HTML。FreshRSS 条目不会出现该来源,只适用于非 FreshRSS 上游。
## 对日报质量的影响
如果提取结果文件中出现 `content_source: item.raw_summary`,说明 AI 使用的是订阅源摘要或片段,而不是完整正文。日报内容显得较浅时,原因可能只是 RSS 描述过短。
提高质量可以选择提供完整 `<content:encoded>` 的订阅源,或者把内容来源切换到支持全文 RSS 的系统,例如具备全文提取能力的 RSS 代理或 FiveFilters 等服务。
## 快速检查
先调用 `list_run_artifacts(run_id)`,再读取返回的提取结果产物路径,检查 `content_source` 和 `article.plain_text`。不要按 `run_id` 或“最新目录”手拼路径。
## 相关代码路径
- `src/summary_mcp/core/pipeline.py`:`RSS_ONLY_UPSTREAMS`、`_should_skip_fetch()`、`extract_content()`。
- `src/summary_mcp/core/content_loader.py`:`choose_inline_content()` 的优先级链和 `fetch_html()`;FreshRSS 条目不会调用后者。
@@ -0,0 +1,17 @@
# 飞书 Markdown 格式说明
## 背景
Hermes 的飞书网关(`gateway/platforms/feishu.py`)通过 `_build_outbound_payload` 发送消息。该方法会检查内容中的 Markdown 特征,并据此决定消息类型:
- 内容匹配 `_MARKDOWN_HINT_RE`(加粗、列表、代码、链接等)时,使用包含 `md` 元素的飞书 `post` 类型发送,可以正常渲染。
- 内容匹配 `_MARKDOWN_TABLE_RE`(Markdown 表头和分隔行)时,整条消息会被强制转换为 `text` 类型,即纯文本,不再渲染 Markdown。
原因是 `_build_markdown_post_payload` 会把内容包装为 `{"tag": "md", "text": "..."}` 元素,而飞书的 `md` 元素不支持表格,也没有把 Markdown 表格转换为飞书原生表格的逻辑。
## 飞书输出规则
- 通过飞书发送的消息不得使用 Markdown 表格;消息中只要出现一个表格,整条消息就会退化为纯文本。
- 需要表达结构化信息时,优先使用分点列表、带标题的分节或行内格式。
- 加粗(`**加粗**`)、行内代码(`` `代码` ``)、无序列表(`- 项目`)、有序列表(`1. 项目`)和链接均可正常使用。
- 围栏式代码块可以使用,但代码块后的尾随内容可能存在渲染边界问题。
@@ -0,0 +1,177 @@
# Reader Digest Flow 操作参考
本文件承载 `reader-digest-flow` 的具体执行步骤。正式行为边界以 `../SKILL.md` 为准。
## 1. 日报 Pipeline
### 默认参数
```json
{
"limit": 7,
"include_read": false,
"mark_read": true,
"debug_artifacts": false,
"timeout_seconds": 60,
"max_retries": 2
}
```
### 正式调用序列
```text
start_freshrss_pipeline_job
→ get_freshrss_pipeline_job_status
→ get_freshrss_pipeline_job_result
→ get_run_status
→ get_delivery_payload / get_run_report
```
状态动作:
- `running`:按合理间隔继续轮询;
- `success`:读取结果,保存 `run_id`;
- `failed`:读取关联 Run 并执行 Resume Plan;
- `partial`:优先检查 Run Report、Artifact 和 Recovery 信息。
恢复序列:
```text
inspect_resume_plan
→ recommended_action=resume
→ start_resume_job
→ get_resume_job_status
→ get_resume_job_result
```
如果建议为 `read_terminal_result`,直接读取结果;如果为 `start_new_run`,停止并等待用户决定。
不要根据 `run_id` 拼目录。后续只消费调用返回的 `output_dir`、`artifact.path`、`delivery_output`、`report_output`。
## 2. 候选汇报与选择
优先使用当前 Run 的 digest brief Artifact;不可用时使用 `get_delivery_payload` 返回的候选。
展示规则:
1. 使用候选数组原始顺序并从 1 编号;
2. 不因 `keep/review` 分组而重新编号;
3. 每篇展示标题、来源、摘要和判断理由;
4. 用户编号只映射当前候选数组;
5. 跨 Artifact 读取详情时按 URL 或完整 `item_id` 匹配。
不要按 `summary-batch`、`item-XX` 和候选数组的相同位置推断它们是同一篇文章。
### extracted 文件与候选编号错位(实测 2026-07-31)
- `digest-brief.json` 的 `top_candidates` **没有 `item_id` 字段**,只有 `url`;
- `extracted/item-XX.extracted.json` 的文件名序号与候选编号**可能不一致**(实例:候选2 = item-05、候选3 = item-02);
- 正确做法:用 **URL 交叉匹配**(归一化 `%3D`→`=` 后逐条比对),或用完整 `item_id`(从 candidate-batch.json 的 `items[i].item_id` 按候选数组顺序取)在 extracted 文件里反查;两者都能验证时优先 item_id。
## 3. Hugo 日报
用户确认发布文章后:
1. 读取 `public-digest-example.md`;
2. 仅使用用户选中的文章生成公开内容;
3. 写入 Hugo 当日页面;
4. 前台执行部署,避免把构建日志作为聊天通知;
5. 验证首页、列表页和详情页。
当前部署位置:
```text
/home/ubuntu/zhu/apps/hugo-site/content/daily/YYYY-MM-DD/index.md
```
验证至少覆盖:
```text
http://127.0.0.1:14322/
http://127.0.0.1:14322/daily/
http://127.0.0.1:14322/daily/YYYY-MM-DD/
```
页面不存在时检查源文件、构建结果、容器状态和页面日期。需要 Docker/Hugo 深度排障时再读取部署项目自己的文档,不把历史事故规则复制回主 Skill。
## 4. 单篇知识笔记
用户确认 IMA 文章后:
1. 从候选中取得 URL 和完整 `item_id`;
2. 从 Run Artifact 中找到匹配的 extracted 文件;
3. 交叉验证 `article.item_id` 或 URL;
4. 对每个单篇文件调用 `start_article_summary_job(extracted_path=<返回的 Artifact 路径>, selected_ids=[<完整 item_id>])`;
5. `selected_ids` 传完整、非空、去除 `cand:` 前缀后的 `item_id`;
6. 从 Job 结果的 `written_paths` 读取 Markdown。
### ⚠️ extracted_path 必须用绝对路径
`summary-mcp` 进程的工作目录是 `/root/.hermes`(不是 reader 项目根)。传相对路径(如 `outputs/freshrss/...`)会直接报 `extracted_path does not exist`。必须传绝对路径:
```text
/home/ubuntu/zhu/github/reader/outputs/freshrss/rerun/<run_dir>/extracted/item-XX.extracted.json
```
### ⚠️ 候选编号 ≠ extracted 文件名顺序
候选数组顺序与 extracted 文件名(`item-01`…`item-07`)**不一定对齐**(实测候选2 落在 item-05)。`digest-brief.json` 的候选**没有 `item_id` 字段**,只有 URL。可靠匹配方法:
1. 从 `candidate-batch.json` 取每项完整 `item_id`(在 `candidate` 嵌套对象里,顶层 `item_key` 只是 `item-XX` 文件名序号);
2. 或按 URL 匹配:归一化(`%3D`→`=`)后与每个 extracted 文件的 `article.url` / `article.canonical_url` 比对;
3. 绝不要按候选位置对应 extracted 文件序号。
```python
def norm(u): return u.replace('%3D','=').replace('%3d','=').strip()
# 对每个 extracted 文件取 norm(article.url),与候选 norm(url) 精确比对
```
正式序列:
```text
start_article_summary_job
→ get_article_summary_job_status
→ get_article_summary_job_result
```
内容依据是 extracted Artifact 中的 `article.plain_text`。不得重新抓取原 URL,不得使用原文之外的知识扩写。
CLI 仅在异步 MCP 不可用或人工排障时使用:
```bash
python scripts/run_article_summaries.py \
--extracted <returned-extracted-path> \
--ids <full-item-id> \
--output-dir <explicit-output-dir>
```
## 5. IMA 上传
用户在知识沉淀阶段的文章选择即为上传授权。
执行顺序:
1. 检查生成的 Markdown 与来源;
2. 文件名规范化为 `<完整文章标题>.md`;
3. 确认目标为 `daily` knowledge base;
4. 执行 preflight、create_media、COS upload、add_knowledge;
5. 验证知识库条目存在。
上传格式与 API 参数分别见:
- `ima-format-quickref.md`
- `ima-upload-api.md`
- `ima-credential-chain.md`
失败时记录发生在 preflight、create_media、COS upload 或 add_knowledge 的具体阶段。不要把失败的 Markdown 改写成 URL 导入或 Notes 类型来绕过错误。
## 6. CLI fallback 原则
CLI 仅在以下场景使用:
- MCP 服务不可用;
- Tool transport/launch 失败且无法取得有效 Job;
- 用户明确要求本地调试;
- 人工排障需要直接检查脚本输出。
如果已取得 `job_id` 或 `run_id`,先查询真实状态,避免因响应超时重复启动任务。
@@ -0,0 +1,27 @@
# IMA 凭证与安全边界
## 必需配置
- `IMA_OPENAPI_CLIENTID`
- `IMA_OPENAPI_APIKEY`
- `IMA_DAILY_KNOWLEDGE_BASE_ID`
- `IMA_DAILY_KNOWLEDGE_BASE_NAME=daily`
优先使用当前进程环境和 IMA Skill 已支持的凭证加载机制。不要在本 Skill 中复制、迁移或重写密钥文件。
## 缺失处理
Preflight 返回凭证缺失或目标知识库无法解析时:
1. 停止上传;
2. 只报告缺失的变量名或配置项;
3. 等待用户或运行环境补齐配置;
4. 配置恢复后重新执行 preflight,不重复生成知识笔记。
## 安全边界
- 不在聊天、日志或命令输出中打印完整 API key、KB ID 或 COS 临时凭证;
- `create_media` 返回的 COS 凭证仅在同一受控进程内传给上传工具,不写入磁盘;
- 不通过拼接 Shell 字符串传递凭证,使用参数数组或 IMA Skill 的封装;
- 不绕过 Hermes 的脱敏机制;若现有工具链无法安全传递凭证,停止并报告;
- 上传结束后不持久化 COS 临时凭证。
@@ -0,0 +1,47 @@
# IMA Markdown 格式速查
## 文件与标题
- 文件名:`<完整文章标题>.md`
- `add_knowledge.title`:完整文章标题,不包含 `.md`
- 上传类型:Markdown 文件,`media_type=7`
- 目标:`daily` knowledge base
## 内容来源
只能使用当前 Run 的可追踪内容:
1. extracted Artifact 的 `article.plain_text`;
2. 对应文章的结构化摘要;
3. digest brief 的 summary 与 highlights。
不得使用通用知识补写原文没有的信息,不设置固定字数或字节数门槛。内容较短时保持简洁并忠于来源。
## 标准结构
```markdown
# 完整文章标题
Source: https://原文链接
Category: 分类
## 核心结论
## 主要论点
## 关键方法 / 机制
## 重要细节
## 可复用启发
## 关键词
## 主题
```
- 核心结论和主要论点使用连贯段落;
- 方法、细节和启发按完整知识点分项;
- 没有来源支持的 Section 可以简写,不得编造内容填充。
上传 API 见 `ima-upload-api.md`。
@@ -0,0 +1,126 @@
# IMA Markdown 上传 API
用于将用户选中的单篇 Markdown 知识笔记上传到 `daily` knowledge base。
## 凭证
- `IMA_OPENAPI_CLIENTID`
- `IMA_OPENAPI_APIKEY`
- `IMA_DAILY_KNOWLEDGE_BASE_ID`
- `IMA_DAILY_KNOWLEDGE_BASE_NAME`
凭证定位和恢复见 `ima-credential-chain.md`。不要在终端输出完整密钥。
## 上传前检查
- 文件名为 `<完整文章标题>.md`;
- `title` 为完整文章标题,不带 `.md`;
- Markdown 符合 `ima-format-quickref.md`;
- 内容可追溯到当前 Run Artifact;
- 用户已经明确选择该文章;
- 目标知识库已经解析并验证。
## 1. Preflight
调用 IMA Skill 的 `preflight-check.cjs` 检查文件类型、扩展名、大小和 MIME。
预期:
```text
file_ext=md
content_type=text/markdown
media_type=7
```
### ⚠️ IMA skill 版本拦截(-200)
`ima_api.cjs` 每天首次调用会检查更新,若检测到新版(如 1.1.8 > 当前 1.1.7)会以 `code=-200` 拦截原请求。注意:**官方 zip 包内的 `meta.json` 可能没同步版本号**(下载 1.1.8 zip 后 meta 仍写 1.1.7),所以光替换文件无法跳过拦截。
快速修复(脚本本身已是新版,只差版本号):
```bash
cd /root/.hermes/skills/openclaw-imports/ima-skill && python3 -c "
import json
m = json.load(open('meta.json')); m['version'] = '1.1.8'
json.dump(m, open('meta.json','w'), ensure_ascii=False, indent=2)
"
```
先用 `diff -rq` 对比 zip 与安装目录:若只有 `.DS_Store`/meta 差异,说明代码已是最新,直接改 meta.json 版本号即可;若脚本有实质差异才需要整体替换。
## 2. Create Media
```text
POST /openapi/wiki/v1/create_media
```
请求核心字段:
```json
{
"file_name": "<完整文章标题>.md",
"file_size": 0,
"content_type": "text/markdown",
"knowledge_base_id": "<daily-kb-id>",
"file_ext": "md"
}
```
保存返回的 `media_id` 和 `cos_credential`。COS 临时凭证只在进程内传递,不打印到聊天或日志。
## 3. COS Upload
使用 IMA Skill 提供的 `cos-upload.cjs`,通过参数数组调用并检查:
- 进程 `returncode`;
- `stderr`;
- HTTP 上传结果。
不要拼接包含凭证的 Shell 字符串,也不要把多条 JSON 响应重定向到同一个文件。
## 4. Add Knowledge
```text
POST /openapi/wiki/v1/add_knowledge
```
核心字段:
```json
{
"media_type": 7,
"media_id": "<media-id>",
"title": "<完整文章标题>",
"knowledge_base_id": "<daily-kb-id>",
"file_info": {
"cos_key": "<cos-key>",
"file_size": 0,
"file_name": "<完整文章标题>.md"
}
}
```
## 5. 验证
上传成功不能只依据本地命令退出码。验证目标 knowledge base 中存在对应标题或返回对象,并向用户报告最终结果。
失败时明确报告发生在 Preflight、Create Media、COS Upload 或 Add Knowledge 的哪一步,不改用 URL 导入或 Notes 类型绕过错误。
## 6. 已知坑:IMA skill 版本拦截(-200)
`ima_api.cjs` 每天首次调用会检查远端版本,若发现新版本(如 1.1.8 > 1.1.7)会以 exit=1 + stderr `{"code":-200}` 拦截**所有** API 调用,原请求不发送。此前遇到过。
处理方式(不必整包替换):
1. 按 stderr 提示下载新版 zip(如 `https://app-dl.ima.qq.com/skills/ima-skills-1.1.8.zip`)并解压;
2. 对比新旧 `ima_api.cjs` 的 md5——zip 内核心脚本常与本地一致,只是 `meta.json` 的 `version` 未同步(zip 内仍写 1.1.7);
3. 若 `ima_api.cjs` 一致,只需把本地 `meta.json` 的 `version` 改为远端版本号即可跳过拦截,无需替换文件。
调用成功后再执行本文件前面的上传流程。
## 6. 版本拦截与批量上传实测(2026-07-31)
- **`-200` skill 更新拦截**:`ima_api.cjs` 每天首次调用检查版本,发现新版时以 code -200 退出并提示更新。下载 zip 后**先对比 `ima_api.cjs` 的 md5**——实测 zip 内脚本与已装版本完全一致,只是 `meta.json` 版本号未同步。此时只需把 `~/.hermes/skills/openclaw-imports/ima-skill/meta.json` 的 `version` 改为最新版即可跳过拦截,无需替换任何脚本。
- **Python 脚本编排上传**比 bash 可靠:bash 拼接含中文文件名/凭证的 curl 易出错。用 `subprocess` 参数数组依次调 `preflight-check.cjs` → `ima_api.cjs check_repeated_names` → `create_media` → `cos-upload.cjs`(`--secret-id/--secret-key/--token` 走参数数组,不打印)→ `add_knowledge`,每步解析返回 JSON,失败即停。
- **批量上传**:4 篇逐个跑同一脚本即可;同名文件先 `check_repeated_names` 确认无重复。
- 凭证从 `IMA_OPENAPI_CLIENTID` / `IMA_OPENAPI_APIKEY` 环境变量读取(`ima_api.cjs` 自动加载),KB ID 用 `IMA_DAILY_KNOWLEDGE_BASE_ID`。
@@ -0,0 +1,29 @@
# 关键词治理路由
关键词治理不属于 `reader-digest-flow` 的日常执行阶段。
仅当用户明确要求“清理关键词”“词库治理”“生成关键词建议”时,委托:
```text
skills/keyword-cleanup-review/SKILL.md
```
Review 输入生成由该 Skill 定义,后续确认与 Apply 由当前编排层负责:
```text
build review bundle
→ generate rule suggestions
→ optional semantic suggestions
→ human review
→ dry-run
→ apply accepted suggestions
```
约束:
- Suggestions JSON 是 Review 与 Apply 之间的正式契约;
- LLM 语义建议不能自动 Apply;
- 不直接编辑 aliases、stopwords、watchlist 或 interest 配置;
- 不在日报主流程中因 tag 质量不佳自动触发治理。
Review bundle、Suggestions、Schema 和产物保留策略以 `keyword-cleanup-review` 为唯一事实来源;该 Skill 不直接 Apply 配置。
@@ -0,0 +1,31 @@
+++
title = "AI 日报 · 示例"
date = 2026-04-01T09:00:00+08:00
summary = "围绕 Agent 架构分层、Skills 标准化与工程实践的当日观察。"
+++
# 今日概览
今天的公开内容主要集中在 AI Agent 架构演进、工具化落地与工程实践三条线索。行业关注点正在从“模型能做什么”转向“系统如何稳定落地并持续复用”。
## 今日重点
### 1. 从 Agent 到 Skills:AI 智能体架构的范式转变
文章分析了 AI 智能体从单体 Agent 向模块化 Skills 的演进,并结合 MCP、Skills 和真实项目说明能力分层与复用方式。
值得关注:
- Skills 将领域流程从 Agent 主体中拆出,便于复用和维护。
- MCP 为 Agent 与外部工具提供标准化连接方式。
- 工程竞争点逐渐从模型调用转向状态、工具和工作流设计。
这篇内容值得关注的原因在于,它把开放协议、分层架构和真实落地案例连接成了完整论证链。
来源:[示例来源](https://example.com/a)
## 趋势观察
1. Agent 正在从单体能力转向可组合的模块化体系。
2. 工具契约、状态管理和验证机制正在成为 AI 应用的核心工程能力。
3. Human-in-the-loop 仍是控制高风险副作用的重要边界。
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""IMA 上传单篇 Markdown 知识笔记到 daily 知识库。
用法: python3 ima_upload_one.py "<绝对路径/summary.md>" "<完整文章标题>"
依赖环境变量: IMA_OPENAPI_CLIENTID / IMA_OPENAPI_APIKEY / IMA_DAILY_KNOWLEDGE_BASE_ID
流程: preflight -> check_repeated_names -> create_media -> cos-upload -> add_knowledge
说明: 用 Python 而非 bash 编排,避免中文文件名/引号转义问题。
退出码 2 = 文件名重复(需与用户确认保留双方或取消),非 0 均为失败。
"""
import json, os, subprocess, sys
SKILL_DIR = "/root/.hermes/skills/openclaw-imports/ima-skill"
IMA_API = os.path.join(SKILL_DIR, "ima_api.cjs")
COS_UPLOAD = os.path.join(SKILL_DIR, "knowledge-base/scripts/cos-upload.cjs")
PREFLIGHT = os.path.join(SKILL_DIR, "knowledge-base/scripts/preflight-check.cjs")
def run_node(script, args):
r = subprocess.run(["node", script] + args, capture_output=True, text=True, timeout=120)
if r.returncode != 0:
raise RuntimeError(f"{script} exit={r.returncode} stderr={r.stderr[:500]}")
return json.loads(r.stdout)
def ima_api(api_path, body):
r = subprocess.run(["node", IMA_API, api_path, json.dumps(body, ensure_ascii=False)],
capture_output=True, text=True, timeout=120)
if r.returncode != 0:
raise RuntimeError(f"ima_api {api_path} exit={r.returncode} stderr={r.stderr[:500]}")
resp = json.loads(r.stdout)
if resp.get("code") != 0:
raise RuntimeError(f"ima_api {api_path} code={resp.get('code')} msg={resp.get('msg')}")
return resp.get("data", {})
def main():
kb_id = os.environ["IMA_DAILY_KNOWLEDGE_BASE_ID"]
file_path = sys.argv[1]
title = sys.argv[2] # 完整文章标题(不含 .md)
pf = run_node(PREFLIGHT, ["--file", file_path])
if not pf.get("pass"):
raise RuntimeError(f"preflight failed: {pf}")
file_name = pf["file_name"]; media_type = pf["media_type"]
content_type = pf["content_type"]; file_size = pf["file_size"]; file_ext = pf["file_ext"]
print(f"[preflight] ok file={file_name} ext={file_ext} size={file_size} media_type={media_type}")
dup = ima_api("openapi/wiki/v1/check_repeated_names", {
"params": [{"name": file_name, "media_type": media_type}],
"knowledge_base_id": kb_id
})
is_rep = dup.get("results", [{}])[0].get("is_repeated", False) if dup.get("results") else False
if is_rep:
print(f"[check_repeated_names] REPEATED: {file_name} — 需要处理")
sys.exit(2)
print("[check_repeated_names] no duplicate")
cm = ima_api("openapi/wiki/v1/create_media", {
"file_name": file_name,
"file_size": file_size,
"content_type": content_type,
"knowledge_base_id": kb_id,
"file_ext": file_ext
})
media_id = cm["media_id"]
cos = cm["cos_credential"]
print(f"[create_media] media_id={media_id} cos_bucket={cos.get('bucket_name')} cos_key={cos.get('cos_key','')[:40]}")
r = subprocess.run(["node", COS_UPLOAD,
"--file", file_path,
"--secret-id", cos["secret_id"],
"--secret-key", cos["secret_key"],
"--token", cos["token"],
"--bucket", cos["bucket_name"],
"--region", cos["region"],
"--cos-key", cos["cos_key"],
"--content-type", content_type,
"--start-time", str(cos.get("start_time", "")),
"--expired-time", str(cos.get("expired_time", "")),
"--timeout", "300000"
], capture_output=True, text=True, timeout=360)
if r.returncode != 0:
raise RuntimeError(f"cos-upload exit={r.returncode} stderr={r.stderr[:800]}")
print(f"[cos-upload] ok rc=0 stdout={r.stdout.strip()[:200]}")
ak = ima_api("openapi/wiki/v1/add_knowledge", {
"media_type": media_type,
"media_id": media_id,
"title": title,
"knowledge_base_id": kb_id,
"file_info": {
"cos_key": cos["cos_key"],
"file_size": file_size,
"file_name": file_name
}
})
print(f"[add_knowledge] ok media_id={ak.get('media_id') or media_id}")
if __name__ == "__main__":
main()