# Article Candidate / OpenClaw Input / Daily Digest 设计 ## 1. 文档目的 本文档正式定义 OpenClaw 日报链路中的三层对象: - `ArticleCandidateRecord` - `OpenClawCandidateInput` - `DailyDigest` 目标不是只定义字段,而是明确每一层对象的职责边界,避免把“内部记录对象”和“发给 OpenClaw 的下游输入对象”混成同一个 payload。 --- ## 2. 这次为什么要改 前一版设计里,`article_candidate` 同时承担了三种职责: 1. 本地审计与追溯 2. 规则过滤后的内部候选记录 3. 发给 OpenClaw 的下游输入 这会直接带来三个问题: - 字段过重 - `article.plain_text` 这类正文内容会显著增加 token 消耗 - 字段过细 - `filter_decision.matches`、`matched_rules`、`labels` 这类规则证据链更适合本地审计,不适合发给下游聚合器 - 字段过耦合 - `source_refs` 是本地文件系统路径,对 OpenClaw 没有意义,反而会让上下游绑定内部实现 批量跑完 `FreshRSS -> extraction -> LLM summary -> filter -> article_candidate` 后,这个问题已经非常清楚: - 本地对象需要尽量保留信息,便于追溯误判 - OpenClaw 输入需要尽量精简,便于聚合日报和控制 token 因此,正确改法不是继续给一个 `article_candidate` 做加减法,而是把对象拆层。 --- ## 3. 设计结论 建议正式拆成三层: ### 3.1 `ArticleCandidateRecord` 定位: - 内部候选记录对象 - 面向本地留档、审计、调试、复跑 它回答的问题是: - 这条候选内容从哪里来 - 提取、摘要、过滤各阶段到底产出了什么 - 为什么规则引擎会给出当前决策 - 后续如果要重放或排查,该去哪里追溯 ### 3.2 `OpenClawCandidateInput` 定位: - 发给 OpenClaw 的精简输入对象 - 面向日报聚合与编排 它回答的问题是: - 这条内容是什么 - 为什么值得放进候选池 - 应该如何排序和栏目化 它不负责保存本地调试信息,也不负责承载完整正文。 ### 3.3 `DailyDigest` 定位: - OpenClaw 聚合后的日级主产物 - 面向日报汇报、知识沉淀、人工 review 它回答的问题是: - 今天最值得关注的内容是什么 - 这些内容能提炼出什么结论 - 哪些内容值得进入长期知识库 --- ## 4. 推荐链路 推荐把链路明确为: `item -> article -> summary -> filter_result -> ArticleCandidateRecord -> OpenClawCandidateInput -> DailyDigest` 这里有两个关键转换: 1. `summary + filter_result` 先生成 `ArticleCandidateRecord` - 这是内部标准记录 2. `ArticleCandidateRecord` 再投影成 `OpenClawCandidateInput` - 这是跨系统传输对象 这样做的本质是: - 内部对象追求可追溯 - 下游对象追求低耦合、低 token、高可消费性 --- ## 5. `ArticleCandidateRecord` 设计 ### 5.1 角色定义 `ArticleCandidateRecord` 是当前项目内部的正式候选记录对象。 它不是: - 发给 OpenClaw 的最终 payload - 发给用户的日报消息 - 最终知识库对象 它是下游所有再加工动作之前的“内部事实底稿”。 ### 5.2 设计原则 - 保留上游对象,便于调试和重放 - 保留规则证据链,便于解释误判 - 保留本地引用,便于审计 - 允许后续重新生成不同版本的下游 payload ### 5.3 建议字段 ```json { "candidate_id": "cand:sha256:xxx", "item": {"...": "Item"}, "article": {"...": "ExtractedArticle"}, "summary": {"...": "LlmSummaryResult"}, "filter_result": {"...": "FilterDecisionResult"}, "digest_section_hint": "insights", "digest_rank": 60, "review_state": "pending", "rendered_markdown": null, "metadata": { "generated_at": "2026-03-25T10:00:00Z", "pipeline_version": "v2", "producer": "summary_mcp", "run_id": "candidate-2026-03-25-001" }, "source_refs": { "item_path": "outputs/freshrss/items/batch/item-01.item.json", "extracted_path": "outputs/freshrss/extracted/batch/item-01.extracted.json", "summary_path": "outputs/freshrss/summary/batch/item-01/result.loop.json", "filter_path": "outputs/freshrss/filter/batch/item-01.filter.json" } } ``` ### 5.4 字段说明 - `candidate_id` - 候选记录唯一标识 - 建议基于 `item_id` 或 `extract_id` 派生 - `item` - 保留来源、标题、发布时间、作者等上游元数据 - `article` - 保留正文提取结果与质量标记 - 供本地调试与重放使用 - `summary` - 保留结构化摘要结果 - 是后续 OpenClaw 输入映射的主要来源 - `filter_result` - 保留规则引擎的完整裁决与命中细节 - 主要用于解释和排查 - `digest_section_hint` - 给 OpenClaw 的栏目建议 - `digest_rank` - 给 OpenClaw 的排序信号 - `review_state` - 记录人工复核状态 - `rendered_markdown` - 可选的人类可读卡片 - 用于调试或 fallback 展示 - `metadata` - 记录运行时元信息 - `source_refs` - 仅用于本地追溯 - 不应进入跨系统 payload ### 5.5 为什么内部对象要保留正文和完整过滤结果 因为这层对象不是给 OpenClaw 直接消费的,而是给系统内部留档的。 如果这里过早裁掉字段,会丢失两种关键能力: 1. 误判排查能力 - 例如文章被误判为 `paywall` 时,必须能回看 `article` 与 `filter_result` 2. 下游重放能力 - 后面如果调整了 OpenClaw payload 或摘要策略,可以从这层重新投影,而不必重新抓取全文 --- ## 6. `OpenClawCandidateInput` 设计 ### 6.1 角色定义 `OpenClawCandidateInput` 是当前项目发给 OpenClaw 的正式输入对象。 它应当是: - 扁平化的 - 精简的 - 稳定的 - 不依赖本地文件路径的 ### 6.2 设计原则 - 不携带正文全文 - 不携带规则引擎的完整证据链 - 不携带本地 `source_refs` - 只保留 OpenClaw 聚合日报真正需要的字段 ### 6.3 建议字段 ```json { "candidate_id": "cand:sha256:xxx", "title": "套壳中国大模型撑起500亿美元估值?扒一扒 Cursor 的套壳疑云", "url": "http://www.ruanyifeng.com/blog/2026/03/kimi-cursor.html", "published_at": "2026-03-21T10:19:11Z", "author": "阮一峰", "source_name": "阮一峰博客", "summary": "2 到 3 句话摘要", "highlights": ["...", "...", "..."], "keywords": ["Cursor", "Kimi K2.5"], "topics": ["人工智能", "商业伦理"], "category": "观点评论", "worth_keeping": true, "worth_reason": "有持续性的行业洞察价值", "selection_decision": "review", "selection_reason": "worth_keeping=true but no stronger keep rule matched", "digest_section_hint": "insights", "digest_rank": 60 } ``` ### 6.4 字段说明 - `candidate_id` - 与内部记录保持同一主键,便于回溯 - `title` / `url` / `published_at` / `author` / `source_name` - OpenClaw 做日报聚合所需的最小来源信息 - `summary` / `highlights` / `keywords` / `topics` / `category` - OpenClaw 聚合时真正需要的语义材料 - `worth_keeping` / `worth_reason` - 摘要层提供的长期价值判断信号 - `selection_decision` / `selection_reason` - 规则层的压缩结果 - 用于告诉 OpenClaw:这条为什么进入候选池,以及应该怎样对待 - `digest_section_hint` / `digest_rank` - OpenClaw 做栏目化和排序时最直接的信号 ### 6.5 为什么不用正文全文 因为 OpenClaw 当前承担的是“日报聚合”角色,不是“再次全文阅读器”。 如果把 `article.plain_text` 一并发给 OpenClaw,会出现三个问题: 1. token 浪费 - 每条都携带全文,批量聚合时成本会迅速膨胀 2. 角色混乱 - OpenClaw 会被迫重新阅读原文,而不是消费上游已经压缩好的摘要结果 3. 输入不稳定 - 正文长度差异很大,会让聚合阶段的上下文更难控制 因此,正文应保留在 `ArticleCandidateRecord`,而不是进入 `OpenClawCandidateInput`。 ### 6.6 为什么过滤结果要压缩 完整的 `filter_result` 适合本地审计,不适合跨系统传输。 OpenClaw 通常只需要知道: - 这条是 `keep / review / drop` 中的哪一种 - 为什么会进入候选池 - 优先级大概是多少 它通常不需要知道: - 具体命中了哪几条规则 - 每条规则携带了哪些 labels - 完整的 `matches` 证据链 所以建议把规则层压缩为: - `selection_decision` - `selection_reason` - `digest_rank` ### 6.7 为什么不要 `source_refs` 因为 `source_refs` 是本地实现细节,不是业务语义。 把它发给 OpenClaw 的问题有两个: 1. 没价值 - OpenClaw 无法消费本地磁盘路径 2. 高耦合 - 这会让 OpenClaw 输入隐式依赖当前项目的目录结构与输出命名方式 因此,`source_refs` 应只保留在 `ArticleCandidateRecord`。 --- ## 7. 两个对象之间的映射关系 建议映射规则如下: - `OpenClawCandidateInput.candidate_id` - 来自 `ArticleCandidateRecord.candidate_id` - `title` / `url` / `published_at` / `author` - 优先来自 `item` - `source_name` - 优先来自 `item.source_title`,没有时可退化为域名或作者来源 - `summary` / `highlights` / `keywords` / `topics` / `category` - 来自 `summary` - `worth_keeping` / `worth_reason` - 来自 `summary.worth_keeping` 与 `summary.reason` - `selection_decision` - 来自 `filter_result.decision` - `selection_reason` - 可取 `filter_result.reasons[0]` 或压缩后的组合说明 - `digest_section_hint` - 来自 `ArticleCandidateRecord.digest_section_hint` - `digest_rank` - 来自 `ArticleCandidateRecord.digest_rank` 简化理解: - `ArticleCandidateRecord` 保留证据 - `OpenClawCandidateInput` 保留结论 --- ## 8. `DailyDigest` 设计 ### 8.1 角色定义 `DailyDigest` 是 OpenClaw 聚合多个 `OpenClawCandidateInput` 后形成的日级主产物。 它不应该再携带单篇全文,也不应该回退到内部调试结构。 ### 8.2 建议字段 ```json { "digest_id": "digest:2026-03-25", "date": "2026-03-25", "title": "2026-03-25 阅读日报", "summary": "今天的输入主要围绕 AI 工具、工程实践和软件产品策略展开。", "sections": [ { "section_id": "insights", "title": "观察与判断", "summary": "今天更值得关注的是 AI 对软件行业护城河和商业包装的影响。", "items": [ { "candidate_id": "cand:sha256:xxx", "title": "文章标题", "summary": "单篇摘要压缩版", "why_it_matters": "为什么今天值得被放进这个栏目", "action": "值得继续跟进" } ] } ], "top_items": ["cand:sha256:xxx"], "key_takeaways": [ "摘要层和知识沉淀层应分开。", "规则证据链应保留在本地,而不是直接下发给聚合器。" ], "watchlist": [ "继续收敛 paywall 误判。" ], "candidate_ids": ["cand:sha256:xxx"], "source_refs": [ { "candidate_id": "cand:sha256:xxx", "url": "https://example.com/post/1" } ], "editor_notes": "今天的输入以观点和周刊类内容为主。", "stats": { "candidate_total": 12, "kept_total": 4, "review_total": 6, "dropped_total": 2 }, "metadata": { "generated_at": "2026-03-25T21:00:00Z", "producer": "openclaw", "pipeline_version": "v2" } } ``` ### 8.3 `sections.items[]` 子结构建议 ```json { "candidate_id": "cand:sha256:xxx", "title": "文章标题", "summary": "单篇摘要压缩版", "why_it_matters": "为什么这条内容今天值得看", "action": "值得试用 / 值得跟进 / 值得收藏 / 待验证" } ``` 这里最关键的不是原始摘要复述,而是 `why_it_matters`。 因为日报的价值不在于再列一遍新闻,而在于表达“今天为什么值得看”。 --- ## 9. 为什么这样设计更合理 ### 9.1 控制 token 成本 把正文全文挡在 OpenClaw 之前,能显著降低聚合阶段的 token 开销。 ### 9.2 保持对象职责单一 - `ArticleCandidateRecord` 负责本地审计 - `OpenClawCandidateInput` 负责下游消费 - `DailyDigest` 负责日级成品 每层只做一件事,后续更容易演进。 ### 9.3 降低上下游耦合 OpenClaw 不需要知道本地 `outputs/` 目录结构,也不应该依赖规则引擎的内部细节。 ### 9.4 保留调试与重放能力 内部对象依然保留全文、质量标记、完整过滤证据链,因此不会因为“下游精简”而损失工程可维护性。 ### 9.5 为后续人审与知识库沉淀留出口 当日报和知识库真正接起来时: - OpenClaw 基于精简输入做日报聚合 - 人工基于日报成品做最终判断 - 本地记录对象作为审计底稿长期保留 这个边界是清晰且可持续的。 --- ## 10. 当前阶段的实现建议 建议按这个顺序推进: 1. 在文档层正式采用这三个对象 2. 将当前代码里的 `article_candidate` 重命名或重新定位为 `ArticleCandidateRecord` 3. 新增 `OpenClawCandidateInput` 模型 4. 增加 `ArticleCandidateRecord -> OpenClawCandidateInput` 的转换逻辑 5. 让 OpenClaw 只消费精简输入对象 6. 后续再补 `DailyDigest` 的真实生成逻辑 --- ## 11. 一句话结论 这次改动的本质,不是“删掉几个字段”,而是把“内部候选记录对象”和“发给 OpenClaw 的精简输入对象”彻底分层;只有这样,当前项目才能同时保留审计能力、控制 token 成本,并稳定服务 OpenClaw 的日报聚合。