From keji-skills
Generates an AI/tech news daily digest from configured newsletters, RSS feeds, and web sources. Deduplicates, scores, and outputs Markdown for Obsidian.
How this skill is triggered — by the user, by Claude, or both
Slash command
/keji-skills:ai-news-kejiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
从用户配置的信息源生成适合 Obsidian 使用的 AI/科技新闻原始稿和摘要稿。
README.en.mdREADME.mdagents/openai.yamlconfig.example.yamlprompts/summary-template.mdreferences/filter-rules.example.mdreferences/init-flow.mdrequirements.txtscripts/_runtime.pyscripts/analyze-source-freshness.pyscripts/build-summary-context.pyscripts/check-run-state.pyscripts/doctor.pyscripts/fetch-aihot.pyscripts/fetch-email-imap.pyscripts/fetch-rss.pyscripts/init.pyscripts/init_wizard.pyscripts/normalize-external-source.pyscripts/sync-hermes-skill.sh从用户配置的信息源生成适合 Obsidian 使用的 AI/科技新闻原始稿和摘要稿。
所有面向用户的说明、进度更新、错误解释和最终回复都必须使用中文。命令、文件名、配置键、环境变量、URL、产品名和 Newsletter 名称保持原样。
启动工作流时不要用英文开场,例如不要说 “I'll start...”。应使用类似“我先检查 ai-news-keji 的初始化状态。”这样的中文说明。
.venv/bin/python,优先从实际开发仓库根目录运行,而不是盲目假设 ~/.hermes/skills/... 就是可执行 repo。先用 scripts/doctor.py 确认当前环境指向的真实仓库路径;如果 doctor 输出了开发仓库(例如 ~/Code/ai-news-keji),后续 init.py / check-run-state.py / build-summary-context.py / fetch 脚本都从那个 repo root 执行,确保使用同一份 .venv、prompts/summary-template.md 和最新脚本。config.yaml,只通过 email.imap.username_env / password_env 指定的环境变量提供。check-run-state.py 显示 has_existing=true,且现有产物只有缓存、原始稿/摘要稿缺失,这类情况默认视为“补充抓取 / 补全产物”,优先复用已有缓存、补抓当前可用来源、重建 summary-context.md,然后补写原始稿和摘要稿;不要把这种 cache-only partial run 直接当成需要覆盖重跑的信号。websites.json 之后,必须确认当前 build-summary-context.py 是否真的把网站条目纳入 summary-context.md。如果没有纳入,就要么先补齐脚本/流程再依赖这些网站条目,要么明确把网站来源降级为“已缓存但未进入本轮 compact context”,不要声称它已经参与了原始稿/摘要稿生成。email-raw.json / 其他缓存,避免安全扫描把结构化缓存误判为可疑内容。所有文件都相对于 skill 目录解析,也就是包含 SKILL.md 的目录。
config.yamlconfig.example.yamlsources.yamlsources.example.yamlscripts/init.pyscripts/init_wizard.pyscripts/check-run-state.pyprompts/summary-template.mdscripts/doctor.pyscripts/fetch-aihot.pyscripts/fetch-rss.pyscripts/fetch-email-imap.pyscripts/normalize-external-source.py运行工作流前:
.venv/bin/python scripts/init.py --check。.venv/bin/python -m pip install -r requirements.txt,然后重试一次检查。config.yaml、缺少 sources.yaml、setup.initialized is not true,或“尚未完成初始化向导”,进入 Agent 分步初始化流程。优先使用 init.py --check 输出里的“建议开场”和“第 1 步问题”作为下一条用户消息;如果检查输出里没有这两段,运行 .venv/bin/python scripts/init.py 获取同样的分步提示。config.example.yaml 或 sources.example.yaml,也不展示 Newsletter、IMAP、输出目录或个人偏好问题。AskUserQuestion 让用户用选项卡形式作答,不要让用户在普通对话里手敲文字回答。每步的选项设计、字段、文案细节见 references/init-flow.md;以 init.py / init_wizard.py 输出的"建议开场"和分步问题为准。用户提交答案后再推进下一步。.venv/bin/python scripts/init.py --answers-file <answers.json>。JSON 字段:external_skills、newsletter.choice、newsletter.host、newsletter.folder、newsletter.username_env、newsletter.password_env、output_dir、preferences。.venv/bin/python scripts/init.py --check。只有检查通过后,才继续读取 config.yaml、sources.yaml 并抓取新闻。--reconfigure:先运行 .venv/bin/python scripts/init.py --reconfigure {external_skills|newsletter|output_dir|preferences} 获取该步的 AskUserQuestion 指令,收集答案后运行 .venv/bin/python scripts/init.py --answers-file <answers.json> --reconfigure <section> 写回 config.yaml,其他步骤保持原值不动。区分:仅追加安装一个外部 skill(不改任何已有答案)才用 --skills <name>;任何涉及"重新选 / 改回答"的场景都走 --reconfigure。~ 和环境变量。使用 config.yaml 控制行为:
paths.output_dir:原始稿和摘要 Markdown 的写入目录paths.filter_rules:评分规则和兴趣画像;未设置或文件缺失时使用内置示例paths.cache_dir:原始来源数据和滚动去重状态的缓存目录settings.default_date:yesterday 或 todaysettings.retention_days:缓存清理窗口settings.dedup_window_days:跨天去重窗口settings.timezone:日期过滤时区pipeline.enabled_sources:要尝试的来源组,例如 aihot、rss、email、external_skills、websitespipeline.skip_unavailable_sources:缺少工具或凭据时跳过对应来源,而不是直接失败email.mode:none、imap 或 mcpemail.imap.*:IMAP host、folder 和凭据环境变量名external_skills.*:可选命令;只运行 enabled: true 的条目notification.method:none、macos 或其他用户配置方式配置不确定时,运行健康检查:
.venv/bin/python scripts/doctor.py
缺少本地配置时,在 Agent 对话中收集初始化答案,然后写入临时 JSON 并运行:
.venv/bin/python scripts/init.py --answers-file <answers.json>
抓取来源前,工作流必须通过这个检查:
.venv/bin/python scripts/init.py --check
初始化检查通过后、抓取任何来源前,必须检查目标日期是否已经有运行产物:
.venv/bin/python scripts/check-run-state.py --date YYYY-MM-DD --config config.yaml
如果输出里的 has_existing 为 true,除非用户在原始请求中明确说“强制刷新 / 覆盖重跑 / 忽略已有文件”,否则必须先停下来询问用户如何处理,不要直接抓取。除了 existing_kinds,还要检查:
is_partial_runpartial_reasonssource_states用单选提问,先说明检测到哪些已有产物、哪些来源只跑了一半,再提供这些选项:
使用已有结果:不抓取新数据。若摘要稿已存在,直接报告原始稿和摘要稿路径;若只有原始稿存在,则基于已有原始稿生成摘要。补充抓取:保留已有原始稿和缓存,只抓取当前可用来源的新内容;如果某个 heavy external source 只有 raw cache、还没完成 normalization,这个选项也负责先补完 normalization,再追加新条目并重写摘要。重新抓取并覆盖:清空目标日期缓存并覆盖当天原始稿和摘要稿;这个选项会覆盖已有结果,必须在说明里明确提醒。sources.yaml 里的每个来源都可以包含 frequency:
| frequency | rule |
|---|---|
daily | 每次运行都检查。 |
weekday | 目标日期是周六或周日时跳过。 |
3x_week | 每次运行都检查;没有新内容是正常情况。 |
weekly | 如果缓存显示最近 7 天已成功抓取,则跳过。 |
irregular | 每次运行都检查;经常没有新内容是正常情况。 |
被跳过的来源不是失败,也不应该生成空章节。
默认使用 settings.default_date 选择的日期。如果用户指定了日期,使用用户指定的日期。
先运行:
.venv/bin/python scripts/check-run-state.py --date YYYY-MM-DD --config config.yaml
如果目标日期已经存在原始稿、摘要稿或缓存:
使用已有结果、补充抓取 或 重新抓取并覆盖。使用已有结果 时,不改写已有原始稿;如摘要稿存在,直接结束并报告路径;如摘要稿不存在,只执行“生成摘要”步骤。补充抓取 时,继续后续抓取流程;若 partial_reasons 显示某个来源只有 raw cache,则先基于现有 raw cache 补跑 normalization,再决定是否需要重抓上游。写入原始稿时只追加新增内容,不删除已有内容。重新抓取并覆盖 时,先说明会覆盖目标日期文件;只有用户明确选择后,才清空 {paths.cache_dir}/YYYY-MM-DD/ 并覆盖 {paths.output_dir}/YYYY-MM-DD.md 与摘要稿。如果没有已有产物,继续正常抓取。
工具可用时,并行抓取已启用的来源组。
AI HOT
如果 pipeline.enabled_sources 启用了 aihot,优先把 AI HOT 作为无需 token 的原生 API 来源抓取。不要安装或调用 AI HOT 的独立 Agent Skill;当前 skill 已经通过稳定 REST API 接入。
.venv/bin/python scripts/fetch-aihot.py --date YYYY-MM-DD --output {cache_dir}/YYYY-MM-DD/aihot.json
.venv/bin/python scripts/normalize-external-source.py --source aihot --input {cache_dir}/YYYY-MM-DD/aihot.json --output {cache_dir}/YYYY-MM-DD/aihot-normalized.json
默认抓 items?mode=selected,脚本会自动带 AI HOT API 要求的浏览器式 User-Agent,并按目标日期过滤 publishedAt。不要手写 curl 省略 UA,也不要把整份 API raw JSON 直接喂给 LLM;后续只让 build-summary-context.py 读取 aihot-normalized.json。
RSS
在 skill 目录运行:
.venv/bin/python scripts/fetch-rss.py --date YYYY-MM-DD --config sources.yaml
如果缺少 sources.yaml,脚本会回退到 sources.example.yaml。
Email Newsletter
只有当 pipeline.enabled_sources 启用了 email 时,才使用已配置的 email 来源白名单。
支持的模式:
none:跳过邮件来源。imap:运行内置 IMAP 抓取脚本。凭据必须来自 email.imap.username_env 和 email.imap.password_env 指定的环境变量。mcp:当当前 Agent 运行环境提供 email/Gmail MCP 工具时使用该工具。使用 IMAP 时,在 skill 目录运行:
.venv/bin/python scripts/fetch-email-imap.py --date YYYY-MM-DD --config config.yaml --sources sources.yaml
IMAP 抓取脚本会搜索目标日期,用 BODY.PEEK[] 读取邮件以避免标记为已读,根据配置的发件人和可选 subject_contains 过滤邮件,尽可能提取纯文本,并把 JSON 输出到 stdout。
使用 MCP 时,搜索目标日期的邮件,根据配置的发件人和可选 subject 规则过滤,读取匹配邮件,并跳过欢迎邮件、订阅确认、纯赞助邮件、广告和招聘内容。
如果 IMAP 凭据或 MCP 工具不可用,且 pipeline.skip_unavailable_sources 为 true,则跳过邮件来源,并记录该来源组不可用。
外部 skills 和 CLI
对每个 enabled: true 的 external_skills.* 条目,运行其配置的命令。命令缺失、目录缺失或非零退出都视为来源失败;如果 pipeline.skip_unavailable_sources 为 true,则跳过并记录原因。
当前柯基的运行配置里,follow-builders 已从 ai-news-keji 主日报来源中移出,改由 Hermes cron follow-builders-standalone-daily 每天 07:00 单独抓取、推送,并写入 每日新闻/Follow Builders/YYYY-MM-DD.md;08:15 的 sync-follow-builders-to-ai-news-summary 会把该 section 同步进当天 YYYY-MM-DD 摘要.md。因此主日报流程不要因为 follow-builders 已安装就主动运行它;只有当 external_skills.follow-builders.enabled: true 时才按下面 heavy-source 流程处理。
对外部 skills 的结果要做可落库性检查。对 aihot、follow-builders(仅当配置显式启用)、bestblogs、ak-rss-digest 这类 heavy external source,必须先缓存 raw,再运行 deterministic normalization,最后才把 normalized items 带进写稿与摘要流程:
.venv/bin/python scripts/normalize-external-source.py --source aihot --input {cache_dir}/YYYY-MM-DD/aihot.json --output {cache_dir}/YYYY-MM-DD/aihot-normalized.json
.venv/bin/python scripts/normalize-external-source.py --source follow-builders --input {cache_dir}/YYYY-MM-DD/follow-builders.json --output {cache_dir}/YYYY-MM-DD/follow-builders-normalized.json
.venv/bin/python scripts/normalize-external-source.py --source bestblogs --input {cache_dir}/YYYY-MM-DD/bestblogs.json --output {cache_dir}/YYYY-MM-DD/bestblogs-normalized.json --deep-read-bestblogs
.venv/bin/python scripts/normalize-external-source.py --source ak-rss-digest --input {cache_dir}/YYYY-MM-DD/ak-rss.json --output {cache_dir}/YYYY-MM-DD/ak-rss-digest-normalized.json
然后再继续下面这些规则:
follow-builders 一类含长 podcast transcript / prompt 模板 / 汇总摘要的来源,默认不能整段直写,必须先拆成独立事件或明确跳过。bestblogs 的 discover ... --json 结果里,readUrl 可能为 null。不要因为首层 JSON 没给链接就判定“无原文链接”。如果条目带有 resourceId,必须先尝试运行 bestblogs read deep <resourceId> 拉取详情;这个详情通常会返回可落库的 canonical URL 和正文 Markdown。bestblogs read deep <resourceId> 成功拿到 URL,就按正常有链接条目处理,并优先使用 deep read 返回的 URL 作为原始稿和摘要稿链接。bestblogs read deep <resourceId> 命中 RATE_LIMITED / 429,不要误记为“BestBlogs 无链接”。应把该来源标记为“本轮 deep read 受限流影响,待下轮补抓”,并保留 discover 层候选信息,方便下一轮或次日重试。bestblogs read deep <resourceId> --json 时,先看 data.meta 是否已经给出 canonical URL / readUrl,再做 RATE_LIMITED / 429 字符串兜底判断;正文或支持链接里可能天然带有 429 数字,不能因此把成功的 deep-read 误判成限流。readUrl 为空、resourceId 不可用,或 bestblogs read deep <resourceId> 明确失败且不是限流时,才把该条目当作 BestBlogs 推荐摘要写入;此时必须明确标注它来自 BestBlogs 推荐、属于二手摘要、缺少直达原文链接,不要把它伪装成已核验的一手来源。aihot、follow-builders、bestblogs、ak-rss-digest 视为 heavy external source:先缓存 raw,再做 deterministic normalization,再把 normalized items 交给去重/评分/写稿流程;不要把整段 transcript、tweet dump、prompt 或聚合摘要直接喂进日报。check-run-state.py 返回 is_partial_run / partial_reasons / source_states 时,优先把它解释为“该日期可能只跑了一半”。如果用户没明确要求覆盖,先选择补完 normalization 或补生成摘要,而不是直接重抓覆盖。doctor.py、AI HOT 实网抓取 smoke test、heavy-source normalization 样本,以及 check-run-state.py 的 partial/complete 两种 fixture。data.meta.url 存在,但 markdown/帮助链接里带 429 数字”的样本;该样本必须仍被判定为成功,不能回退成 rate_limited。用户明确选择外部 skills 后,可通过初始化答案写入配置;如果只是追加安装某个外部 skill,可运行:
.venv/bin/python scripts/init.py --skills follow-builders,bestblogs,ak-rss-digest
初始化脚本会把原始外部仓库安装到 external_skills.install_dir 下,并把选择的 skills 软链到 external_skills.link_targets。
它可以安装并启用:
follow-builders:把 zarazhangrui/follow-builders clone 到受管理的外部 skill 目录,在其 scripts/ 目录运行 npm install,并软链到已配置的 agent skill 目录。bestblogs:安装 @bestblogs/cli,并提示用户运行 bestblogs auth login;不安装 BestBlogs 对话式 agent skills。ak-rss-digest:clone rookie-ricardo/erduo-skills,把其中的 skills/ak-rss-digest 子 skill 链接到受管理的外部 skill 目录,并软链到已配置的 agent skill 目录。网站来源
对 Readwise Weekly 等已配置网站来源,仅在浏览器或 web-fetch 能力可用时抓取。使用缓存避免重复抓取同一期周报。
把原始来源数据写入 {paths.cache_dir}/YYYY-MM-DD/。
建议缓存文件:
email-raw.json
rss-raw.json
external-skills.json
websites.json
aihot.json
aihot-normalized.json
follow-builders.json
follow-builders-normalized.json
bestblogs.json
bestblogs-normalized.json
ak-rss.json
ak-rss-digest-normalized.json
run-manifest.json
原始邮件缓存可能包含私人内容。请把 paths.cache_dir 放在公开 skill 目录之外,且不要提交缓存文件。
如果本轮存在 weekly / frequency skip 的来源,或某些来源以“已检查但无新增”结束,建议额外写一个 run-manifest.json,至少包含:
status: completestatus / completed / notesstatus: skipped 且 completed: true这样 check-run-state.py 才能把“已按规则跳过”与“真的没跑完”区分开,避免完整运行在 cron 场景里被误判成 partial run。
对 heavy external source,后续去重、评分和写稿应优先读取 *-normalized.json,而不是直接消费 raw 输出。
在进入原始稿/摘要稿写作前,先构建 compact context:
.venv/bin/python scripts/build-summary-context.py --date YYYY-MM-DD --config config.yaml --output {cache_dir}/YYYY-MM-DD/summary-context.md
然后必须做一次新鲜度检查:
.venv/bin/python scripts/analyze-source-freshness.py --date YYYY-MM-DD --config config.yaml
这一步不是可选项。它的目标是区分:
新鲜度判断的实现细节也要固定下来,避免误判:
email 的时间字段优先看原始邮件日期(常见为 RFC2822 / UTC 字符串),不要因为格式不同就把整批 newsletter 误判为 undated。bestblogs 的发布时间很多时候不在顶层,而是在 normalized 条目的 deep_read.publish_datetime;做 freshness 统计时必须把它提升/回填到 published_at 参与判断。summary-context.md 里必须同时输出两层信息:一层是来源级统计(dated / fresh_48h / recent_7d),另一层是候选条目上的 published=... 与 freshness=... 标记,方便写稿时区分“今天新内容”和“近几天延续内容”。后续 LLM 步骤只读取 summary-context.md、prompts/summary-template.md 和评分规则。不要再把整份 raw JSON、normalized JSON、完整原始稿或长 transcript 拉进上下文。需要核验时,只回到单条来源链接或单个缓存文件,不要整源重读。
如果 cron / agent 环境与交互式 shell 使用了不同 Python,优先保证所有 repo 命令从仓库根目录用 .venv/bin/python 执行。对可能被外部调度器从错误解释器拉起的脚本,可以加一个很薄的 runtime guard:先检查必需模块,再自动 re-exec 到 repo-local .venv/bin/python,最后才继续导入 feedparser、yaml 等依赖。
如果本轮改动了交付文件(尤其 email-raw.json、原始稿、摘要稿、summary-context.md、run-manifest.json)且仓库没有 canonical test/lint/build 命令,结束前必须做一次 ad-hoc verification:
tempfile 创建带 hermes-verify- 前缀的临时 Python 脚本;不要把验证脚本长期留在 repo 或缓存目录。email-raw.json 结构正常;原始稿和摘要稿没有 {{...}} 模板残留;关键章节标题存在。.venv/bin/python scripts/check-run-state.py --date YYYY-MM-DD --config config.yaml,并断言 is_partial_run=false。hermes-verify-*.py 临时脚本再跑一遍。清理早于 settings.retention_days 的日期缓存目录。
维护 {paths.cache_dir}/recent-events.json,保存最近 settings.dedup_window_days 天的事件。
对每个抓取项:
recent-events.json 比较,判断是否属于旧事件延续报道。settings.dedup_window_days 的旧记录。保留延续报道时,标记为 continuation,并在评分时应用 settings.continuation_penalty。如果同一事件已经在最近 3 天进入过“今日行业大事”,默认不要连续两天再次放入“今日行业大事”;只有确认存在实质新信息时才允许再次进入,并在文案里明确写出新增了什么。
写入或追加 {paths.output_dir}/YYYY-MM-DD.md。
原始稿要求:
created、updated、type 和 sources如果文件已存在,追加新抓取的来源章节,不删除已有内容。
如果 {paths.filter_rules} 存在则读取它,否则使用 references/filter-rules.example.md。
优先读取 {paths.cache_dir}/YYYY-MM-DD/summary-context.md;只有在核验单条事实或修正链接时才回看原始稿/单个缓存文件。不要把完整原始稿重新喂给模型。
应用三层处理:
prompts/summary-template.md,写入 {paths.output_dir}/YYYY-MM-DD 摘要.md,覆盖该日期的旧摘要。模板里所有 {{...}} 占位符必须替换为实际值,不能保留花括号、也不能整段省略。规则:
--- 分隔。analyze-source-freshness.py 显示硬新闻来源没有 48 小时内的新内容,不要把最近几天的 newsletter / AI HOT 条目伪装成“今天主线”;必须在文案里明确说明它们是近几天的延续。如果 notification.method 为 macos,只在 macOS 上使用 osascript。如果通知不可用或设为 none,静默结束,并用中文向用户报告生成的文件路径。
npx claudepluginhub lovekeji-ai/keji-skills --plugin keji-skillsScrapes preset URLs, filters high-quality technical content, and generates daily Markdown reports with multi-agent orchestration and browser scraping.
Tracks AI news from 80+ entities across 6 free sources (Reddit, HN, GitHub, HuggingFace, arXiv, X/Twitter). Generates scored daily Markdown reports with deduplication and cross-verification.
Manages AI News Radar: finding high-signal AI/tech sources, adding RSS/OPML/GitHub feeds, checking source health, updating the web UI, GitHub Actions, or GitHub Pages deployment.