Refine OpenClaw payloads and reorganize docs

This commit is contained in:
zhuyongxin
2026-03-26 10:20:07 +08:00
parent cecf4d3ae7
commit 27fe1e8882
155 changed files with 8999 additions and 87 deletions
+225
View File
@@ -0,0 +1,225 @@
# 规则过滤引擎设计
## 1. 目标
当前过滤层的定位是:
- 不让 LLM 直接做最终过滤决策
- 让 LLM 只产出结构化信号
- 由规则引擎输出最终 `keep / drop / review` 决策
当前链路是:
`item -> content extraction -> llm summary -> filter rule engine`
## 2. 输入输出
### 2.1 FilterInput
过滤层统一读取四类输入:
- `item`
- `article`
- `summary`
- `context`
其中:
- `item` 表示上游标准化候选条目
- `article` 表示正文提取结果
- `summary` 表示结构化 LLM 摘要结果
- `context` 表示额外的用户偏好或运行时上下文
### 2.2 FilterDecisionResult
过滤结果统一输出:
- `decision`
- `keep`
- `drop`
- `review`
- `matched_rules`
- `reasons`
- `labels`
- `priority`
- `matches`
这样后续的知识库 sink、推送层或人工审核都可以稳定消费。
## 3. 规则结构
当前规则文件位置:
- `configs/filter_rules.json`
单条规则结构为:
```json
{
"rule_id": "keep-worth-keeping-method",
"enabled": true,
"stop_on_match": false,
"conditions_all": [
{ "field": "summary.worth_keeping", "op": "eq", "value": true },
{ "field": "summary.category", "op": "in", "value": ["方法论", "工具实践"] }
],
"action": {
"decision": "keep",
"reason": "Structured summary marked the content as worth keeping in a durable category.",
"labels": ["summary", "durable"],
"priority": 80
}
}
```
## 4. 当前支持的操作符
- `eq`
- `ne`
- `in`
- `not_in`
- `contains`
- `overlap`
- `gte`
- `lte`
- `exists`
当前条件组合方式:
- `conditions_all`
- `conditions_any`
## 5. 动态上下文字段
规则支持从其他字段动态取值,例如:
```json
{ "field": "summary.topics", "op": "overlap", "value": { "from_field": "context.interest_topics" } }
```
这允许上层 Agent 在调用 `filter_summary_result` 时,把当前关注主题动态注入,而不必把偏好硬编码在规则文件里。
## 6. 当前默认规则思路
当前默认规则分为三类:
- 质量拦截
- 低质量正文直接 `drop`
- 疑似截断或付费墙进入 `review`
- 摘要价值判断
- `资讯 + worth_keeping=false` 直接 `drop`
- `方法论/工具实践 + worth_keeping=true` 直接 `keep`
- 个性化补充信号
- `summary.topics` 与 `context.interest_topics` 重叠时提升为 `keep`
- 其他 `worth_keeping=true` 的结果默认进入 `review`
## 7. 规则执行流程
当前过滤流程按以下顺序执行:
1. 上层先产出结构化 `summary`
2. 可选附带 `item`、`article` 与 `context`
3. 规则引擎按 `priority` 从高到低遍历规则
4. 每条规则根据 `conditions_all` / `conditions_any` 判断是否命中
5. 命中的规则被收集为 `matches`
6. 最终根据命中结果收敛为 `keep / drop / review`
当前决策收敛规则是:
- 只要命中任意 `drop`,最终结果就是 `drop`
- 否则只要命中任意 `keep`,最终结果就是 `keep`
- 否则如果命中任意 `review`,最终结果就是 `review`
- 如果没有任何规则命中,默认回落到 `review`
这样设计的原因是:
- `drop` 应该拥有最高约束力
- `keep` 只在没有硬性淘汰时生效
- 默认不自动放行未知内容
## 8. 为什么让 LLM 调用规则 tool,而不是直接裁决
当前架构故意拆成两层:
- LLM 负责生成结构化信号
- 规则引擎负责输出最终过滤决策
原因是:
- LLM 适合做语义理解、归类、摘要和价值信号提取
- 规则引擎适合做稳定、可复现、可审计的最终判断
因此推荐的调用方式是:
1. LLM 先调用提取 tool
2. LLM 或本地脚本产出 `summary_result`
3. LLM 再调用 `filter_summary_result`
4. 后续根据过滤结果决定是否入库、推送或人工审核
这意味着:
- LLM 是编排者
- rule engine 是裁决器
而不是让 LLM 在过滤阶段再次自由发挥。
## 9. 调用示例
### 9.1 本地脚本
```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
```
### 9.2 带上下文的调用
```json
{
"interest_topics": ["个人信息管理", "阅读工作流"]
}
```
将这份上下文作为 `--context` 传入后,规则就可以根据当前关注主题做加权。
### 9.3 MCP tool
`filter_summary_result` 接收:
- `summary_result`
- 可选 `extracted_article`
- 可选 `item`
- 可选 `context`
返回:
- `decision`
- `matched_rules`
- `reasons`
- `labels`
- `priority`
- `matches`
## 10. 当前落地位置
- 过滤模型:
- `src/summary_mcp/models/filtering.py`
- 规则引擎:
- `src/summary_mcp/filters/engine.py`
- 默认规则:
- `configs/filter_rules.json`
- 本地运行脚本:
- `scripts/run_filter_rules.py`
- MCP tool:
- `filter_summary_result`
## 11. 当前阶段的设计结论
第一版过滤层采用“规则主导,LLM 提供信号”的架构:
- LLM 不直接做最终裁决
- 上层 Agent 可以决定是否调用过滤 tool
- 真正的过滤决策由规则引擎输出
- 灰区内容后续再考虑是否引入第二层 LLM 辅助判断
+105
View File
@@ -0,0 +1,105 @@
# Markdown Sink 设计
## 1. 目标
第一版 sink 不直接绑定外部平台,而是先落一个稳定的 `Markdown sink`。
原因:
- 可审计
- 可版本化
- 易迁移
- 可直接兼容 Obsidian / 通用 Markdown 知识库
## 2. 统一输入
Markdown sink 读取统一的 `SinkInput`:
- `item`
- `article`
- `summary`
- `filter_decision`
- `metadata`
这样未来扩展 Notion sink、Webhook sink 时,可以继续复用同一套上游输入结构。
## 3. 目录结构
当前建议目录结构:
```text
knowledge-base/
inbox/
review/
archive/
```
具体落盘规则:
- `keep` -> `knowledge-base/inbox/YYYY/YYYY-MM/`
- `review` -> `knowledge-base/review/YYYY/YYYY-MM/`
- `drop` -> `knowledge-base/archive/YYYY/YYYY-MM/`
## 4. 文件命名
文件名格式:
```text
YYYY-MM-DD-title-slug.md
```
例如:
```text
2026-03-21-kimi-cursor.md
```
## 5. Markdown 内容结构
输出采用:
- YAML frontmatter
- 摘要区
- 要点区
- 过滤决策区
- 原文信息区
- 正文摘录区
frontmatter 中保留:
- `title`
- `url`
- `source_id`
- `item_id`
- `extract_id`
- `category`
- `worth_keeping`
- `decision`
- `priority`
- `topics`
- `keywords`
- `labels`
- `published_at`
- `saved_at`
- `quality_flags`
## 6. 当前落地位置
- sink 模型:
- `src/summary_mcp/models/sink.py`
- Markdown sink:
- `src/summary_mcp/sinks/markdown.py`
- 本地运行脚本:
- `scripts/run_markdown_sink.py`
## 7. 当前阶段结论
第一版知识库不先绑定 Notion、Lumina 或数据库,而是先让过滤后的内容稳定进入 Markdown 知识库。
只要这层格式稳定,后续可以继续扩展:
- Obsidian Vault
- Logseq
- Notion
- Webhook
- 自定义数据库 sink
+544
View File
@@ -0,0 +1,544 @@
# Source Schema 设计草案
## 1. 文档目的
本文档用于定义阅读流系统中的来源与内容对象模型,目标是把“来源分类”的讨论收敛成一套可执行的数据结构,供后续的抓取、摘要、过滤、入库和推送流程统一使用。
这份文档关注的是数据抽象,而不是具体实现语言、数据库模型或 API 细节。
---
## 2. 设计目标
这套 schema 需要解决以下问题:
- 不同渠道的内容如何进入同一条流水线
- RSS、微信文章、普通网页、GitHub 更新如何被统一表示
- 摘要器应该接收什么输入对象
- 过滤规则应该依赖哪些标准字段
- 入库与推送如何复用统一元信息
核心目标是建立稳定的中间层,避免后续各模块直接依赖渠道差异。
---
## 3. 设计原则
### 3.1 分层而不是混合
建议将系统中的对象分成三层:
- `source`
- `item`
- `document`
每一层职责不同:
- `source`:描述一个来源渠道本身
- `item`:描述一次抓取到的候选内容
- `document`:描述经过正文抽取和摘要后的标准内容对象
### 3.2 先标准化,再智能化
在进入摘要、过滤、入库前,应该先把不同渠道的内容统一映射成相同的对象结构,而不是在每个处理阶段临时兼容各种来源格式。
### 3.3 渠道类型与内容形态分离
建议明确区分:
- 来源渠道是什么
- 内容本身是什么形态
例如:
- 微信文章和 RSS 博文可能都属于 `article`
- GitHub Release 和 changelog 可能来自不同渠道,但内容形态更接近事件流
---
## 4. 对象分层
### 4.1 `source`
`source` 是来源配置对象,用来描述某个渠道本身是什么、如何接入、是否启用。
特点:
- 偏静态
- 由用户或系统配置维护
- 不代表具体某一篇内容
### 4.2 `item`
`item` 是采集层产物,用来表示“这次抓回来的一个候选条目”。
特点:
- 来自 RSS、API、HTML 抓取或转换 feed
- 可能只有标题、链接和摘要
- 不保证已经拿到完整正文
### 4.3 `document`
`document` 是标准内容对象,用来表示“已经过正文抽取和摘要处理,可以进入过滤、入库和推送阶段”。
特点:
- 已经完成正文抽取
- 已经有结构化摘要
- 是后续规则引擎的主输入
---
## 5. 枚举设计
### 5.1 `source_type`
表示来源类型。
可选值:
- `feed`
- `converted_feed`
- `event_feed`
- `page_watch`
- `custom_source`
说明:
- `feed`:原生 RSS / Atom
- `converted_feed`:由 RSSHub、wewe-rss、RSS-Bridge 等转换而来
- `event_feed`:更像事件流,例如 GitHub Releases、Commits、Discussions
- `page_watch`:轮询普通网页的更新情况
- `custom_source`:为特殊站点定制的抓取源
### 5.2 `ingest_mode`
表示接入方式。
可选值:
- `feed_direct`
- `feed_converted`
- `api_backed`
- `html_scraped`
- `hybrid`
说明:
- `feed_direct`:直接消费 RSS / Atom
- `feed_converted`:先转换成 feed,再统一消费
- `api_backed`:通过 API 获取数据
- `html_scraped`:直接抓 HTML 页面并解析
- `hybrid`:先消费 feed 元数据,再按需抓正文
### 5.3 `content_kind`
表示内容形态。
可选值:
- `article`
- `thread`
- `release`
- `changelog`
- `video`
- `mixed`
说明:
- `article`:标准文章
- `thread`:串联式内容,例如帖子串、社交媒体长线程
- `release`:版本发布说明
- `changelog`:更新日志
- `video`:视频内容
- `mixed`:无法明确归入单一形态
### 5.4 `fetch_state`
表示候选内容的正文抓取状态。
可选值:
- `pending`
- `fetched`
- `failed`
- `skipped`
### 5.5 `pipeline_state`
表示内容在整条流水线中的处理状态。
可选值:
- `ingested`
- `summarized`
- `filtered`
- `stored`
- `pushed`
- `dropped`
---
## 6. `source` 对象设计
### 6.1 角色定义
`source` 用于描述一个来源本身,而不是某条具体内容。
它应该回答这些问题:
- 这个来源是什么
- 它属于哪一类来源
- 用什么方式接入
- 是否启用
- 默认语言和默认内容形态是什么
### 6.2 建议字段
```json
{
"source_id": "wechat-aiweekly",
"name": "AI Weekly 微信源",
"source_type": "converted_feed",
"ingest_mode": "feed_converted",
"content_kind": "article",
"base_url": "https://example.com",
"feed_url": "https://example.com/feed.xml",
"language": "zh",
"enabled": true,
"tags": ["ai", "newsletter"],
"priority": 80
}
```
### 6.3 字段说明
- `source_id`
- 系统内部唯一标识
- `name`
- 人类可读名称
- `source_type`
- 来源类型
- `ingest_mode`
- 接入方式
- `content_kind`
- 默认内容形态
- `base_url`
- 来源站点主域名
- `feed_url`
- feed 地址;没有则可为空
- `language`
- 默认语言
- `enabled`
- 是否启用该来源
- `tags`
- 来源级标签,供过滤和统计使用
- `priority`
- 用于抓取调度、结果排序或推送优先级控制
### 6.4 最小可用版本
```json
{
"source_id": "my-blog",
"source_type": "feed",
"ingest_mode": "feed_direct",
"feed_url": "https://example.com/rss.xml",
"enabled": true
}
```
---
## 7. `item` 对象设计
### 7.1 角色定义
`item` 是采集层的标准对象,用来统一表示从不同渠道抓回来的候选内容。
它应该回答这些问题:
- 这条候选内容来自哪个 source
- 它的标题、链接和发布时间是什么
- RSS 是否已经提供摘要或正文
- 当前是否已抓到完整正文
### 7.2 建议字段
```json
{
"item_id": "sha256:xxx",
"source_id": "wechat-aiweekly",
"external_id": "feed-entry-id-or-url",
"title": "文章标题",
"url": "https://example.com/post/123",
"author": "作者",
"published_at": "2026-03-23T08:00:00Z",
"discovered_at": "2026-03-23T10:00:00Z",
"content_kind": "article",
"language": "zh",
"raw_summary": "RSS 提供的摘要",
"raw_content": "RSS 提供的正文或片段",
"metadata": {
"feed_title": "源标题",
"categories": ["AI", "Tools"]
},
"fetch_state": "pending"
}
```
### 7.3 字段说明
- `item_id`
- 内部唯一标识
- 建议由 `source_id + canonical_url + published_at` 做 hash 生成
- `source_id`
- 指向来源配置对象
- `external_id`
- 来源平台本身的 id,例如 RSS entry id、原始 URL 或平台内容 id
- `title`
- 候选内容标题
- `url`
- 原始内容链接
- `author`
- 作者信息;如果无法获取可为空
- `published_at`
- 原始发布时间
- `discovered_at`
- 被系统发现的时间
- `content_kind`
- 当前条目的内容形态
- `language`
- 当前条目的语言
- `raw_summary`
- 来源提供的摘要内容
- `raw_content`
- 来源直接提供的正文、正文片段或富文本转纯文本结果
- `metadata`
- 保留来源特有元信息,例如 feed 分类、栏目名、标签
- `fetch_state`
- 正文抓取状态
### 7.4 最小可用版本
```json
{
"item_id": "sha256:xxx",
"source_id": "my-blog",
"title": "文章标题",
"url": "https://example.com/post/1",
"published_at": "2026-03-23T08:00:00Z",
"raw_summary": "摘要",
"raw_content": "",
"fetch_state": "pending"
}
```
---
## 8. `document` 对象设计
### 8.1 角色定义
`document` 是经过正文抽取和摘要处理后的标准内容对象。
它应该回答这些问题:
- 最终正文是什么
- 摘要和高亮点是什么
- 这个内容值不值得进入下一步过滤或入库
- 当前它处于流水线哪个阶段
### 8.2 建议字段
```json
{
"document_id": "sha256:yyy",
"item_id": "sha256:xxx",
"source_id": "wechat-aiweekly",
"url": "https://example.com/post/123",
"title": "文章标题",
"author": "作者",
"published_at": "2026-03-23T08:00:00Z",
"language": "zh",
"content_kind": "article",
"plain_text": "抽取后的正文",
"summary": "3-5句摘要",
"highlights": ["要点1", "要点2", "要点3"],
"keywords": ["rss", "mcp"],
"topics": ["workflow", "knowledge-base"],
"scores": {
"relevance": 0.91,
"novelty": 0.72,
"readability": 0.84
},
"quality_flags": {
"is_paywalled": false,
"is_truncated": false,
"is_low_content": false
},
"pipeline_state": "summarized"
}
```
### 8.3 字段说明
- `document_id`
- `document` 的唯一标识
- `item_id`
- 回溯到原始候选条目
- `source_id`
- 回溯到来源配置
- `url`
- 原始链接
- `title`
- 标题
- `author`
- 作者信息
- `published_at`
- 发布时间
- `language`
- 语言
- `content_kind`
- 内容形态
- `plain_text`
- 抽取后的正文纯文本
- `summary`
- 结构化摘要中的主摘要
- `highlights`
- 关键要点
- `keywords`
- 关键词
- `topics`
- 主题标签
- `scores`
- 打分结果,例如相关性、新颖度、可读性
- `quality_flags`
- 内容质量标志,例如是否付费墙、是否截断、是否低信息密度
- `pipeline_state`
- 当前流水线状态
### 8.4 最小可用版本
```json
{
"document_id": "sha256:yyy",
"item_id": "sha256:xxx",
"title": "文章标题",
"url": "https://example.com/post/1",
"plain_text": "正文",
"summary": "3-5句摘要",
"highlights": ["点1", "点2"],
"scores": {
"relevance": 0.9
},
"pipeline_state": "summarized"
}
```
---
## 9. 三层对象之间的关系
建议关系如下:
- 一个 `source` 可以产生多个 `item`
- 一个 `item` 在多数情况下对应一个 `document`
- `document` 是 `item` 经过正文抽取和摘要处理后的结果
简化理解:
- `source` 解决“从哪里来”
- `item` 解决“抓到了什么”
- `document` 解决“它值不值得留下”
---
## 10. 为什么必须分层
如果不区分 `source`、`item` 和 `document`,后面会出现这些问题:
- 来源配置和运行时数据混在一起
- RSS 字段、网页抓取字段和摘要字段混在一起
- 过滤规则既要判断来源,又要判断内容质量,还要判断摘要分数
- 不同来源的差异会不断渗透到下游模块
分层后的收益是:
- 采集层可以独立演进
- 摘要器输入更稳定
- 过滤器只依赖统一对象,不依赖来源细节
- 入库和推送更容易复用
---
## 11. 当前阶段的最小实现建议
为了降低复杂度,第一阶段建议:
- 只支持 `feed` 和 `converted_feed`
- 只实现 `source`、`item`、`document` 这三层基础对象
- `item` 只要求能容纳 RSS 拉回来的标题、链接、摘要、正文片段
- `document` 只要求能容纳正文、摘要、高亮点和基础打分
这样做的好处是:
- 结构足够稳定
- 成本足够低
- 可以直接支撑下一步 `summary-core` 设计
---
## 12. 后续可继续讨论的问题
1. 是否需要给 `source` 增加抓取周期、超时、重试策略字段
2. `item_id` 和 `document_id` 的生成策略是否要统一
3. `metadata` 是否需要按来源类型拆出专用字段
4. `document` 的 `scores` 和 `quality_flags` 是否要定义更严格的 schema
5. 这套 schema 最终是落成 JSON 文件、数据库表,还是 Pydantic 模型
---
## 13. 当前阶段的一句话结论
当前最合适的做法是先固定 `source -> item -> document` 这三层对象模型,再围绕它设计 `summary-core`、过滤器和入库流程。
@@ -0,0 +1,503 @@
# Summary Core Interface 设计草案
## 1. 文档目的
本文档用于定义 `summary-core` 的输入输出接口,目标是把“页面摘要能力”从概念讨论收敛成一套稳定、可复用、可封装的数据接口。
这份文档不限定实现语言,也不限定它最终通过 CLI、HTTP、MCP 还是 Skill 暴露,只关注能力边界和数据契约。
---
## 2. 角色定位
`summary-core` 是阅读流系统中的摘要内核,负责把候选内容转换成可进入过滤、入库和推送阶段的标准化内容对象。
它在整条链路中的位置是:
`RSS 聚合 / 页面抓取 -> summary-core -> rule-engine -> sink -> push`
它的职责是:
- 接收一个候选内容对象或与之等价的输入
- 获取正文或补全正文
- 抽取正文纯文本
- 生成结构化摘要
- 输出标准化的 `document` 对象或与之兼容的结果
它不负责:
- 维护 RSS 订阅源
- 执行规则过滤
- 写入知识库
- 执行消息推送
- 负责编排整个自动化流程
---
## 3. 设计原则
### 3.1 输入以 `item` 为主,补充字段为辅
`summary-core` 的标准输入应优先基于 `item`,这样可以和上游采集层稳定衔接。
但为了适配不同调用方式,也允许附带额外输入:
- `raw_html`
- `rss_content`
- `user_profile`
- `summary_style`
### 3.2 输出以 `document` 为中心
`summary-core` 的输出应该尽量直接映射到 `document`,而不是返回一段松散文本。
这样后续的规则引擎、知识库入库和推送层都可以稳定消费。
### 3.3 失败也要结构化表达
正文抓取失败、内容过短、抽取结果异常、疑似付费墙等情况,不能只返回报错字符串,应该返回结构化错误或状态标志。
### 3.4 同一套数据契约服务多种封装方式
不管未来是:
- 本地 CLI
- HTTP API
- MCP tool
- OpenClaw Skill
都应尽量复用同一套输入输出 schema,而不是每个入口都定义一套不同的数据格式。
---
## 4. 处理阶段拆分
`summary-core` 内部建议拆成以下几个阶段:
1. 输入标准化
2. 正文获取
3. 正文抽取
4. 摘要生成
5. 质量标记
6. 输出映射
### 4.1 输入标准化
目标:
- 统一不同调用来源的数据结构
- 优先使用 `item`
- 补齐缺失字段
### 4.2 正文获取
目标:
- 优先使用上游已提供的 `raw_content`
- 若内容不足,再根据 `url` 回源抓取页面
- 如有 `raw_html`,优先直接使用
### 4.3 正文抽取
目标:
- 从 HTML 或富文本中抽取正文
- 生成稳定的纯文本内容
- 识别是否正文过短、被截断或包含大量噪音
### 4.4 摘要生成
目标:
- 基于正文生成结构化摘要
- 输出摘要、高亮点、关键词、主题和评分
- 保证结构化输出稳定,不允许自由散文式结果
### 4.5 质量标记
目标:
- 判断是否存在付费墙、截断、低信息密度等问题
- 为后续过滤器提供可用标志位
### 4.6 输出映射
目标:
- 将摘要结果映射为标准 `document`
- 保持与上游 `item` 的可追溯关系
---
## 5. 输入模型
### 5.1 标准输入结构
建议标准输入命名为 `SummaryInput`。
```json
{
"item": {
"item_id": "sha256:xxx",
"source_id": "my-blog",
"title": "文章标题",
"url": "https://example.com/post/1",
"published_at": "2026-03-23T08:00:00Z",
"raw_summary": "RSS 摘要",
"raw_content": "RSS 正文片段",
"content_kind": "article",
"language": "zh"
},
"raw_html": "<html>...</html>",
"rss_content": "RSS 提供的正文内容",
"user_profile": {
"interests": ["AI", "workflow"],
"languages": ["zh", "en"]
},
"summary_style": "default"
}
```
### 5.2 输入字段说明
- `item`
- 标准主输入
- 推荐必填
- `raw_html`
- 可选
- 当调用方已经抓到网页 HTML 时可直接传入,避免重复抓取
- `rss_content`
- 可选
- 当调用方希望优先使用 RSS 正文时可单独传入
- `user_profile`
- 可选
- 用于后续个性化摘要、主题提取或兴趣打分
- 第一阶段可以不启用,但接口层建议预留
- `summary_style`
- 可选
- 用于控制摘要策略,例如 `default`、`brief`、`detailed`、`bullet`
### 5.3 最小可用输入
第一阶段建议只要求:
```json
{
"item": {
"item_id": "sha256:xxx",
"title": "文章标题",
"url": "https://example.com/post/1",
"raw_summary": "RSS 摘要",
"raw_content": ""
}
}
```
---
## 6. 输出模型
### 6.1 标准输出结构
建议标准输出命名为 `SummaryOutput`。
```json
{
"success": true,
"document": {
"document_id": "sha256:yyy",
"item_id": "sha256:xxx",
"source_id": "my-blog",
"url": "https://example.com/post/1",
"title": "文章标题",
"author": "作者",
"published_at": "2026-03-23T08:00:00Z",
"language": "zh",
"content_kind": "article",
"plain_text": "抽取后的正文",
"summary": "3-5句摘要",
"highlights": ["要点1", "要点2", "要点3"],
"keywords": ["rss", "mcp"],
"topics": ["workflow", "knowledge-base"],
"scores": {
"relevance": 0.91,
"novelty": 0.72,
"readability": 0.84
},
"quality_flags": {
"is_paywalled": false,
"is_truncated": false,
"is_low_content": false
},
"pipeline_state": "summarized"
},
"debug": {
"content_source": "fetched_html",
"extractor": "trafilatura",
"model": "summary-model-name"
},
"warnings": []
}
```
### 6.2 输出字段说明
- `success`
- 是否成功完成摘要流程
- `document`
- 标准输出主体
- 结构应尽量与 `document` schema 对齐
- `debug`
- 可选
- 记录本次实际用了什么正文来源、抽取器和模型
- 便于后续排查问题
- `warnings`
- 可选
- 用于记录非致命异常,例如正文过短、内容疑似截断、元信息缺失
### 6.3 最小可用输出
```json
{
"success": true,
"document": {
"document_id": "sha256:yyy",
"item_id": "sha256:xxx",
"title": "文章标题",
"url": "https://example.com/post/1",
"plain_text": "正文",
"summary": "3-5句摘要",
"highlights": ["点1", "点2"],
"scores": {
"relevance": 0.9
},
"pipeline_state": "summarized"
},
"warnings": []
}
```
---
## 7. 错误模型
### 7.1 为什么要有独立错误模型
如果 `summary-core` 只在失败时返回字符串错误,会导致:
- 上层流程难以判断是否应该重试
- 过滤器和编排层难以做自动决策
- MCP 或 Skill 封装时难以标准化输出
因此建议引入结构化错误模型。
### 7.2 建议错误结构
```json
{
"success": false,
"error": {
"code": "CONTENT_FETCH_FAILED",
"message": "Failed to fetch article content",
"retryable": true,
"stage": "fetch",
"details": {
"url": "https://example.com/post/1"
}
},
"warnings": []
}
```
### 7.3 建议错误码
- `INVALID_INPUT`
- `CONTENT_FETCH_FAILED`
- `CONTENT_EXTRACTION_FAILED`
- `CONTENT_TOO_SHORT`
- `PAYWALL_DETECTED`
- `SUMMARY_GENERATION_FAILED`
- `UNSUPPORTED_CONTENT_KIND`
- `UNKNOWN_ERROR`
### 7.4 建议错误字段
- `code`
- 机器可识别错误码
- `message`
- 人类可读错误信息
- `retryable`
- 是否建议重试
- `stage`
- 出错阶段,例如 `normalize`、`fetch`、`extract`、`summarize`
- `details`
- 补充调试信息
---
## 8. 内容来源优先级策略
为了保证 `summary-core` 在不同输入条件下行为稳定,建议明确正文来源优先级。
推荐顺序:
1. `raw_html`
2. `item.raw_content`
3. `rss_content`
4. 根据 `item.url` 回源抓取
解释:
- 如果调用方已经传入 `raw_html`,优先直接用,避免重复请求
- 如果 RSS 已经提供了高质量正文,可优先利用
- 如果 RSS 内容不足,再回源抓页面
这一策略需要在实现阶段进一步细化,例如通过长度阈值判断是否需要回源抓取。
---
## 9. 与 `document` schema 的关系
`summary-core` 的输出不应另起炉灶,而应尽量直接映射到 `document`。
建议遵守以下原则:
- `SummaryOutput.document` 与 `document` schema 保持高度一致
- `summary-core` 只负责生成 `document`,不负责直接写入知识库
- 上游保留 `item_id` 和 `source_id`,确保可追溯
这样做的好处是:
- 下游过滤器只需要消费 `document`
- 存储层无需理解摘要实现细节
- MCP 封装和 CLI 调用都可以复用同一结果结构
---
## 10. 调用方式兼容性
同一套接口建议兼容以下几种封装方式:
### 10.1 CLI
适合:
- 本地验证
- 单条 URL 调试
- 批处理脚本调用
建议形式:
- `summary-core input.json`
- `summary-core --url https://example.com/post/1`
### 10.2 HTTP API
适合:
- 被其他服务调用
- 作为统一后端能力暴露
建议形式:
- `POST /summarize`
### 10.3 MCP Tool
适合:
- 给 OpenClaw、ChatGPT、Claude 等 Agent 调用
建议形式:
- `summarize_item`
- `summarize_url`
### 10.4 Skill
适合:
- 让 Agent 在上下文中决定是否调用摘要流程
- 与其他工具组合编排
结论:
接口 schema 应保持统一,不因为调用方式不同而拆成不同语义模型。
---
## 11. 当前阶段的最小可用接口
第一阶段建议只实现一个最小接口:
输入:
```json
{
"item": {
"item_id": "sha256:xxx",
"title": "文章标题",
"url": "https://example.com/post/1",
"raw_summary": "RSS 摘要",
"raw_content": ""
}
}
```
输出:
```json
{
"success": true,
"document": {
"document_id": "sha256:yyy",
"item_id": "sha256:xxx",
"title": "文章标题",
"url": "https://example.com/post/1",
"plain_text": "正文",
"summary": "3-5句摘要",
"highlights": ["点1", "点2"],
"scores": {
"relevance": 0.9
},
"pipeline_state": "summarized"
},
"warnings": []
}
```
这个阶段先不要求:
- 个性化摘要
- 多风格摘要模板
- 批量接口
- 复杂质量评分
- 高级缓存策略
---
## 12. 后续可继续讨论的问题
1. `SummaryInput` 是否必须包含完整 `item`,还是允许只传 URL
2. `summary_style` 是否应该在第一阶段就进入 schema
3. `debug` 字段是否应面向开发环境可选开启
4. 错误码是否需要更细分成抓取类、抽取类、模型类
5. `plain_text` 是否需要附带 token 数、字数等统计信息
6. `SummaryOutput` 是否需要保留原始抽取文本和最终摘要之间的映射信息
---
## 13. 当前阶段的一句话结论
当前阶段最合理的做法是先固定 `summary-core` 的统一输入输出契约,让它稳定接收 `item`,稳定产出 `document`,再在此基础上封装 CLI、MCP 和 Skill。
+229
View File
@@ -0,0 +1,229 @@
# Summary Loop 工作说明
## 1. 目的
本文档说明 `scripts/run_summary_loop.py` 如何把以下流程串成一个最小闭环:
`提取 JSON -> LLM 生成摘要 JSON -> validator 校验 -> 失败则修复重试`
这个脚本的目标不是单次调用 LLM,而是让 LLM 输出进入一个“生成后验收”的自动反馈回路。
---
## 2. 核心思路
这个闭环由三部分组成:
- 提取结果
- 来自 `outputs/reference/extracted/*.json` 或 `outputs/freshrss/extracted/*.json`
- 提供标题、链接、正文、质量标记
- LLM 摘要
- 基于提取结果和 prompt 生成 `result.json`
- validator
- 检查 `result.json` 是否符合结构和业务规则
- 不通过则返回错误列表
因此,LLM 不是“自己知道哪里错了”,而是脚本在每轮生成后用 validator 明确指出错误,再要求它修复。
---
## 3. 运行顺序
脚本入口:
- `scripts/run_summary_loop.py`
执行顺序如下:
1. 读取提取结果 JSON
2. 读取摘要 prompt 模板
3. 从提取结果中裁剪出摘要真正需要的输入字段
4. 调用 LLM 生成摘要 JSON
5. 解析模型输出中的 JSON
6. 将结果写入输出文件
7. 调用 validator 校验结果
8. 如果通过,结束
9. 如果失败,构造修复 prompt,再次调用 LLM
---
## 4. 输入来源
脚本主要使用两个输入文件:
- 提取结果:`outputs/reference/extracted/read-flow-2026.extracted.json`
- 摘要 prompt:`outputs/prompts/llm-summary-prompt.txt`
提取结果不会整包无差别塞给 LLM,而是先裁剪成更小的摘要输入:
- `article.title`
- `article.url`
- `article.plain_text`
- `article.quality_flags`
- `warnings`
这样做是为了减少 payload,降低模型超时概率,也让 prompt 更聚焦。
---
## 5. 首次生成
第一次生成时,脚本会把:
- prompt 模板
- 结构化文章输入
拼成一个完整请求,发给 LLM。
对应函数:
- `build_initial_prompt(...)`
- `call_llm(...)`
模型返回后,脚本会尝试提取一个 JSON 对象。
如果模型输出不是合法 JSON:
- 这一轮直接视为失败
- 会生成一份失败校验结果
- 然后进入修复重试
---
## 6. validator 如何加入流程
validator 不是附加步骤,而是主流程里的硬门槛。
对应函数:
- `validate_llm_result(...)`
校验内容包括两层:
### 6.1 结构校验
- 必填字段是否存在
- 字段类型是否正确
- `category` 是否属于允许枚举
- `summary` 长度是否合规
- `highlights` / `keywords` / `topics` 数量是否合规
### 6.2 业务校验
- `keywords` 和 `topics` 不能重复
- 列表内部不能重复
- `title` 和 `url` 必须与提取结果一致
只有 validator 返回 `valid: true`,这一轮结果才会被接受。
---
## 7. LLM 如何知道自己生成错了
LLM 本身不会主动知道哪些字段不合规。
脚本会在校验失败后,把以下信息重新发给 LLM:
- validator 错误列表
- 原始提取结果
- 当前错误的摘要 JSON
然后要求它:
- 只修复错误字段
- 保留已经正确的字段
- 继续只输出合法 JSON
对应函数:
- `build_repair_prompt(...)`
也就是说:
- prompt 负责提出目标
- validator 负责判错
- repair prompt 负责把错误喂回模型
这三者一起形成自动修复回路。
---
## 8. 时序图
```text
Extracted JSON
|
v
run_summary_loop.py
|
|-- 读取 prompt 模板
|-- 构造摘要输入
|
v
LLM
|
v
摘要 JSON
|
v
validator
|
|-- valid = true ------> 结束
|
|-- valid = false
| |
| |-- errors
| v
| repair prompt
| |
+------> LLM 重试
```
---
## 9. 生成的中间文件
每次执行脚本都会在输出目录下留下调试痕迹:
- `outputs/reference/summary/result.loop.attempt-1.raw.txt`
- 模型原始输出
- `outputs/reference/summary/result.loop.attempt-1.json`
- 提取出的 JSON 结果
- `outputs/reference/summary/result.loop.attempt-1.validation.json`
- 这一轮的校验报告
- `outputs/reference/summary/result.loop.json`
- 当前最终结果
这些文件的价值是:
- 可以回看模型原始输出
- 可以定位是 JSON 解析失败还是 schema 校验失败
- 可以看每轮修复到底修了什么
---
## 10. 当前脚本参数
当前脚本支持:
- `--extracted`
- `--prompt`
- `--output`
- `--max-retries`
- `--timeout`
- `--api-key`
- `--model`
- `--api-url`
这意味着它既可以走环境变量,也可以直接用命令行传入模型配置。
---
## 11. 一句话结论
`run_summary_loop.py` 的本质不是“调一次 LLM”,而是“让 LLM 生成结果后必须经过 validator 验收,失败就拿着错误清单继续修”,直到结果通过或达到重试上限。
+378
View File
@@ -0,0 +1,378 @@
# Content Extract MCP Service 设计草案
## 1. 文档目的
本文档用于定义当前仓库中已经落地的 MCP 服务设计,即“内容提取 MCP”。
目标是明确:
- 这个 MCP 服务当前真正负责什么
- 它对外暴露哪些 tool
- 每个 tool 的输入输出结构是什么
- validator 与 LLM 摘要如何接在 MCP 之后
- 当前 MVP 已完成到哪一层
这份文档描述的是当前真实实现,而不是早期“摘要 MCP”设想。
---
## 2. 服务定位
这个 MCP 服务的角色是:为上层 Agent、OpenClaw 或其他自动化流程提供统一的“文章内容提取”能力。
它在整条链路中的位置是:
`RSS 聚合 -> Content Extract MCP -> LLM 摘要 -> 校验 -> 规则过滤 -> 入库 -> 推送`
它不是完整阅读流系统,也不是摘要服务本身,而是阅读流中的“结构化正文提取层”。
### 2.1 服务负责的事情
- 接收 URL 或标准化 `item`
- 获取正文或补全正文
- 抽取文章标题
- 抽取正文纯文本
- 返回结构化文章对象
- 返回质量标记和结构化错误
### 2.2 服务不负责的事情
- 管理 RSS 订阅源
- 生成摘要
- 主题分类
- 价值判断
- 规则过滤
- 写入知识库
- 发送通知
- 执行全流程编排
结论:
当前 MCP 服务边界应保持干净,只负责把网页或 item 转成结构化文章数据。
---
## 3. 总体架构
建议并且当前实现采用的是:
```text
MCP Server Layer
-> Tool Handlers
-> extraction core
-> normalizer
-> content_loader
-> extractor
-> quality_checker
-> mapper
```
### 3.1 外层 MCP 层职责
- 注册 tools
- 接收和校验 tool 输入
- 调用内部 extraction core
- 将结果包装成 MCP tool 输出
### 3.2 内层 extraction core 职责
- 处理正文获取、标题提取、正文抽取、质量检查、结果映射
- 与具体 MCP SDK 解耦
### 3.3 为什么必须分层
如果把内容提取逻辑直接写死在 MCP handler 里,后续会出现这些问题:
- 业务逻辑难测试
- 协议层与抽取逻辑耦合
- 以后想补 CLI、批处理或其他入口时需要重复实现
因此当前原则是:
- MCP 是外壳
- extraction core 是内核
---
## 4. 当前暴露的 Tools
当前实现只暴露两个 tool:
- `extract_url_content`
- `extract_item_content`
### 4.1 `extract_url_content`
角色:
- 直接输入 URL
- 适合单条文章测试
- 适合手动调试或上层 Agent 直接调用
建议输入:
```json
{
"url": "https://example.com/post/1",
"language_hint": "zh"
}
```
建议输出:
```json
{
"success": true,
"article": {
"extract_id": "sha256:yyy",
"item_id": null,
"source_id": null,
"url": "https://example.com/post/1",
"title": "文章标题",
"language": "zh",
"content_kind": "article",
"plain_text": "抽取后的正文",
"quality_flags": {
"is_paywalled": false,
"is_truncated": false,
"is_low_content": false
},
"metadata": {
"content_source": "fetched_html",
"extractor": "trafilatura",
"char_count": 1234
},
"pipeline_state": "extracted"
},
"warnings": [],
"debug": {
"content_source": "fetched_html",
"extractor": "trafilatura"
}
}
```
### 4.2 `extract_item_content`
角色:
- 输入标准化 `item`
- 与上游 RSS 聚合层对接
- 保留 `item_id`、`source_id` 等追踪信息
建议输入:
```json
{
"item": {
"item_id": "sha256:xxx",
"source_id": "my-blog",
"title": "文章标题",
"url": "https://example.com/post/1",
"published_at": "2026-03-23T08:00:00Z",
"raw_summary": "RSS 摘要",
"raw_content": "RSS 正文片段",
"content_kind": "article",
"language": "zh"
}
}
```
建议输出:
```json
{
"success": true,
"article": {
"extract_id": "sha256:yyy",
"item_id": "sha256:xxx",
"source_id": "my-blog",
"url": "https://example.com/post/1",
"title": "文章标题",
"published_at": "2026-03-23T08:00:00Z",
"language": "zh",
"content_kind": "article",
"plain_text": "抽取后的正文",
"quality_flags": {
"is_paywalled": false,
"is_truncated": false,
"is_low_content": false
},
"metadata": {
"content_source": "item.raw_content",
"extractor": "inline",
"char_count": 1234
},
"pipeline_state": "extracted"
},
"warnings": [],
"debug": {
"content_source": "item.raw_content",
"extractor": "inline"
}
}
```
---
## 5. 统一输出规范
当前两个 tool 都复用同一个输出结构,即 `ExtractionOutput`。
成功时:
- `success = true`
- 返回 `article`
- 可附带 `warnings`
- 可附带 `debug`
失败时:
- `success = false`
- 返回结构化 `error`
- 不返回不完整的 `article`
这样做的好处是:
- 上层流程只需要消费一个稳定 schema
- 后续 LLM 摘要和 validator 可以独立接上
- OpenClaw 或其他 Agent 更容易编排
---
## 6. 错误返回规范
建议错误结构如下:
```json
{
"success": false,
"error": {
"code": "CONTENT_FETCH_FAILED",
"message": "Failed to fetch article content",
"retryable": true,
"stage": "fetch",
"details": {
"url": "https://example.com/post/1"
}
},
"warnings": []
}
```
当前错误码包括:
- `INVALID_INPUT`
- `CONTENT_FETCH_FAILED`
- `CONTENT_EXTRACTION_FAILED`
- `CONTENT_TOO_SHORT`
- `UNKNOWN_ERROR`
使用原则:
- 抓取失败不等于程序崩溃
- 可以重试的错误应标记 `retryable = true`
- 错误应指出具体阶段
---
## 7. 当前技术选型
当前 MVP 使用:
- Python
- MCP Python SDK (`FastMCP`)
- `httpx` 进行网页抓取
- `trafilatura` 进行正文抽取
- `beautifulsoup4` 作为 HTML 兜底解析
- `pydantic` 进行输入输出约束
设计原则是:
- 外层 MCP
- 内层 extraction core
- LLM 摘要和校验作为 MCP 之后的独立环节
---
## 8. 当前目录结构建议
当前实现大致如下:
```text
summary_mcp/
server.py
core/
normalizer.py
content_loader.py
extractor.py
quality_checker.py
mapper.py
pipeline.py
models/
item.py
document.py
summary_io.py
llm_result.py
validators/
llm_result.py
```
说明:
- `server.py`:MCP 服务入口
- `core/`:提取内核
- `models/`:提取和校验相关模型
- `validators/`:LLM 输出校验逻辑
---
## 9. 与 LLM 摘要层的关系
当前 MCP 不负责摘要,但它与摘要层的接口已经明确:
1. MCP 产出结构化 `article`
2. 上层 LLM 根据 `article.title`、`article.url`、`article.plain_text` 等生成摘要 JSON
3. validator 对摘要 JSON 做 schema 校验与业务校验
4. 若不合格,进入修复重试
这条闭环当前已经通过本地脚本验证:
- `scripts/run_summary_loop.py`
因此,当前正确的职责拆分是:
- MCP 负责提取
- LLM 负责摘要
- validator 负责验收
---
## 10. 当前阶段验收结论
当前 MVP 已完成以下验证:
- 真实 URL 可以提取为结构化文章 JSON
- 提取结果可以保存为文件
- 提取结果可以喂给 LLM 生成摘要 JSON
- 摘要 JSON 可以通过 validator 校验
- 整条“提取 -> 摘要 -> 校验”最小闭环已经跑通
---
## 11. 后续扩展方向
后续建议按下面顺序推进:
- 接入真实 RSS 聚合结果并映射为 `item`
- 定义规则过滤层 schema
- 设计知识库入库格式
- 增加批量处理能力
- 引入 Playwright 作为动态页面兜底方案
- 重命名包和项目名,使其与当前职责一致
---
## 12. 当前阶段一句话结论
当前仓库中的 MCP 已经从早期“摘要 MCP”演进为“内容提取 MCP”,它的职责是稳定产出结构化文章 JSON,并把摘要与验收环节留给后续的 LLM 和 validator 流程。