
Qiaomu Ai Radar
- 6 installs
- 15 repo stars
- Updated June 2, 2026
- joeseesun/qiaomu-ai-radar
Fetch AI/tech/creator news, translate weekly briefs, sync to Feishu, and recommend writing topics based on preferences.
About
Daily news aggregation skill that fetches from multiple sources, translates content, syncs to Feishu, and provides topic recommendations. Supports topic fetch and intelligent recommendation modes.
- Fetches, translates, and syncs weekly content summaries to Feishu
- Intelligent topic recommendation based on learner preferences and trends
Qiaomu Ai Radar by the numbers
- 6 all-time installs (skills.sh)
- Ranked #1,691 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joeseesun/qiaomu-ai-radar --skill qiaomu-ai-radarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 15 |
| Last updated | June 2, 2026 |
| Repository | joeseesun/qiaomu-ai-radar ↗ |
What it does
Fetch AI/tech/creator news, translate weekly briefs, sync to Feishu, and recommend writing topics based on preferences.
Files
乔木 AI 雷达
默认规则
每次调用本 skill,先执行模式 A。用户明确要推荐选题时,执行模式 A 后继续模式 B。
不要先问是否抓取。直接运行统一入口,并基于脚本返回的事实报告回复用户。
模式 A:抓取更新
运行:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 run_radar.py --mode fetch该入口会依次完成:
1. 抓取最新内容,写入本周周刊和完整历史。 2. 翻译本周周刊里所有未翻译英文标题。 3. 再次同步本周周刊到飞书。 4. 验证周刊状态。 5. 打开 Obsidian 本周周刊。
报告必须包含:
- 新增文章数。
- 实际翻译标题数。
- 未翻译残留数。
- 飞书同步状态。
- Obsidian 打开状态。
如果翻译 API key、飞书授权、Node/lark-cli、网络或 Obsidian 打开失败,必须如实报告脚本输出的失败原因。不要说“完成”来掩盖失败。
模式 B:推荐选题
运行:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 run_radar.py --mode fetch-and-recommend推荐输出保持中文,优先选择:
- AI agents、MCP/tool use、推理、模型能力、视频/多模态、交互式解释器。
- Show HN 里有具体 demo 的项目。
- 编程手艺、工程哲学、产品设计、认知/心理学。
降低优先级:
- 企业 AI 战略、纯政策争论、DevOps/cloud admin、crypto、政治体育、终端模拟器新闻。
推荐时说明三件事:为什么适合乔木、可以怎么切入、这个选择反映了什么长期偏好。
处理已标记素材
运行:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 run_radar.py --mode process-marked语义:
- [x]且无收藏:归档为已读/不感兴趣。- [x] 收藏:移入收藏精选。- 未勾选条目保留在本周周刊。
验证与调试
Dry-run:
python3 run_radar.py --mode fetch --dry-run --json
python3 verify_weekly.py --json
python3 sync_feishu_weekly.py --dry-run历史说明和版本记录已归档到 docs/history.md。当前 SKILL.md 只保留执行入口和回复约束。
# Runtime state
data/*
!data/.gitkeep
logs/
# Python caches
__pycache__/
*.py[cod]
# Local/editor noise
.DS_Store
.vscode/
.idea/
# Agent runtime counters
.call_count
.circuit_breaker_history
.circuit_breaker_state
.exit_signals
.last_reset
progress.json
status.json
# Databases and logs
*.db
*.db-shm
*.db-wal
*.log
Daily Topic Selector - 架构文档
每日选题助手的系统架构与设计哲学
🔖 版本历史
v2.15 (2026-03-04) - Read→Write 极速过滤翻译 ⚡
问题:v2.14 后台 agent 仍用逐条 Edit(50+ 次 API 调用,~5 分钟)
方案:Read 一次 → 内存中过滤+翻译 → Write 一次(2 次调用,~10 秒)
设计哲学:批量 > 逐条,内存处理最快,消除工具调用开销
v2.10 (2026-01-10) - AI相关性智能过滤 🧹
问题:HN等综合平台全吞模式,铁路/游戏/政治等无关内容进入周刊
方案:延续v2.7哲学——脚本采集,Claude过滤
工作流:采集 → 智能过滤(新增)→ 翻译 → 阅读
过滤标准:
- 保留:AI/ML、编程、创业、认知科学、设计、科技动态
- 删除:政治、体育、纯硬件、交通基建、娱乐八卦
零代码改动:过滤逻辑在SKILL.md工作流中定义,由Claude执行
v2.8 (2026-01-10) - 新增 8 个 AI/商业信息源 📰
新增信息源(每源限制3篇最新文章):
Substack 类(使用通用 substack_generic.py 解析器): 1. DC The Median - 数据科学与AI应用洞察 2. Mark McNeilly - AI新闻周报 "The New News in AI" 3. Business Analytics - 商业分析与数据驱动决策 4. AI Leadership Edge - AI领导力与企业战略 5. ChinAI Newsletter - 中国AI发展追踪(Jeff Ding) 6. Why Try AI - AI工具测评与实操指南
Beehiiv 类(使用通用 beehiiv_generic.py 解析器): 7. The Rundown AI - 每日AI新闻速递(百万订阅) 8. The Neuron Daily - AI新闻与行业洞察
架构变更:
新增:scripts/parsers/substack_generic.py (80行)
- 通用 Substack RSS 解析器
- 自动将基础URL转换为/feed端点
- 工厂函数:parse_dcthemedian, parse_markmcneilly 等
新增:scripts/parsers/beehiiv_generic.py (70行)
- 通用 Beehiiv RSS 解析器
- 处理 rss.beehiiv.com/feeds/xxx.xml 格式
- 工厂函数:parse_therundown, parse_theneuron
修改:config/sources.json
- 新增8个信息源配置
- 每源 limit: 3(只抓最新3篇)
- check_interval_hours: 24(每日检查)
修改:scripts/fetch_updates.py
- 导入新解析器模块
- PARSERS字典注册8个新解析器设计哲学:
- 消除重复:Substack/Beehiiv 各用一个通用解析器,新源只需配置
- 工厂模式:
parse_xxx()函数只是调用parse_substack_rss(source) - 单一真相源:URL和参数在
sources.json,代码只负责解析
v2.7 (2026-01-04) - 周刊工作流 + 零API翻译 📅
核心变更:从日志模式切换到周刊模式,翻译由Claude直接完成
周刊工作流: 1. 采集模式改变:
- 从
content_log.md(日志追加)→{year}-第{week}周.md(按周组织) - 采集时自动写入本周文件,按ISO周数组织
- 新文章按日期分组(
## 2026-01-04)
2. 翻译工作流重构:
- 删除:
llm_utils.py(114行API调用代码) - 原理:能用LLM直接解决的事,不要写脚本
- 新流程:
1. 采集脚本只记录原始数据(英文标题、中文标题都保留) 2. Claude读取文件后用Edit工具批量添加翻译 3. 格式:- **标题**: English Title / 中文翻译
- 收益:零API调用、零依赖、零token成本
3. "值得写"索引:
- 新增
generate_worth_writing.py - 从
state.json提取标记为📒 to_write的文章 - 生成
值得写.md供Claude分析和推荐 - 自动在采集流程后更新
文件变化:
新增:scripts/generate_worth_writing.py (86行) - 生成"值得写"索引
新增:scripts/update_indexes.py (67行) - 一键更新所有索引
修改:scripts/fetch_updates.py
- append_to_log() → append_to_weekly_log()
- 简化翻译逻辑(114行API代码→0行)
- 移除自动索引生成(职责分离)
删除:scripts/shared/llm_utils.py (不再需要)设计哲学的胜利:
- 简单消灭复杂:114行API代码 → 0行,翻译质量更好
- 单一职责:脚本负责数据,Claude负责智能
- 消除边界情况:
- 无需处理API超时、重试逻辑
- 无需处理中英文混合的edge case
- 无需维护翻译prompt模板
- 好品味的代码:让人说"操,原来可以这么简单"
工作流示例:
# === 自动部分(采集) ===
python3 fetch_updates.py
# 1. 同步上次的emoji标记 → state.json
# 2. 采集新文章 → 2026-第1周.md
# 3. 提示下一步操作
# === 手动部分(处理) ===
# 1. Claude批量翻译周刊标题
# Read → Edit → "English Title / 中文翻译"
# 2. 用户阅读周刊,打emoji标记
# ⭐️ 收藏 📒 值得写 ✅ 已读 ❌ 跳过
# 3. 一键更新所有索引
python3 update_indexes.py
# → 同步标记 → 更新收藏索引 → 更新值得写索引文件职责分离:
fetch_updates.py- 只采集,不生成索引update_indexes.py- 只更新索引(打标后运行)- 收藏索引/值得写索引 - 跨周累积,不随周刊清空
关键理解:
- 周刊(
2026-第1周.md)= Inbox(本周待处理,下周清空) - 收藏索引(
⭐️收藏精选.md)= Archive(永久保存,跨周累积) - 值得写(
值得写.md)= 创作清单(待创作的选题)
v2.6 (2026-01-04) - 新增 4 个顶级播客源 🎙️
新增播客源: 1. Lex Fridman Podcast (lexfridman.com)
- AI、科学、哲学深度访谈
- 嘉宾包括顶级研究者、创业者、思想家
- 长篇对话(2-4小时),深度探讨
2. Cognitive Revolution (cognitiverevolution.ai)
- AI 前沿技术与应用深度解析
- AI 创业案例与产业洞察
- 技术趋势前瞻
3. 80,000 Hours Podcast (80000hours.org)
- 有效利他主义(Effective Altruism)
- 职业选择与社会影响力
- AI 安全、全球优先事项
4. Latent Space (latent.space)
- AI 工程实践与产品开发
- AI 创业公司深度访谈
- 技术栈选择与架构设计
技术实现:
- 创建 4 个 podcast parser:
lexfridman.py,cognitiverevolution.py,hours80k.py,latentspace.py - 全部使用 RSS feed(零依赖,简单可靠)
- Latent Space 初始使用 Flightcast URL 超时,改用 Substack
/feedendpoint 成功 - 测试通过,成功采集 40 篇节目(10+10+10+10)
内容矩阵再扩展:
| 维度 | v2.5 覆盖 | v2.6 新增播客 |
|---|---|---|
| AI 技术深度 | 袁超发、HF、Import AI | Lex Fridman(顶级对话)<br>Cognitive Revolution(前沿技术) |
| AI 工程实践 | - | Latent Space(工程与创业) |
| AI 安全/伦理 | - | 80,000 Hours(有效利他) |
| 跨学科视野 | Wait But Why | Lex Fridman(科学/哲学) |
采集内容示例:
- Latent Space: "The Agent Labs Thesis", "Agentic Commerce Protocol"
- Lex Fridman: 对话顶级 AI 研究者与创业者
- Cognitive Revolution: AI 应用案例深度分析
- 80,000 Hours: AI 安全与职业影响力
设计哲学:
- 播客优先 RSS:所有 podcast 都有标准 RSS,稳定可靠
- URL 调试:Latent Space 从 Flightcast → Substack,找到最稳定endpoint
- 零额外依赖:继续复用
web_utils.py的fetch_html()
v2.5 (2026-01-04) - 新增 4 个高质量信息源 🌐
新增信息源: 1. 袁超发技术博客 (yuanchaofa.com)
- LLM/AI 技术深度文章
- 手写大模型组件系列(Transformer、LoRA、GQA等)
- 中文技术内容,工程实践导向
2. Farnam Street Brain Food (fs.blog)
- 认知科学、心理学、决策理论
- 225+ 期周更 newsletter
- 深度思考与智慧洞察
3. Austin Kleon (austinkleon.substack.com)
- 创意写作与艺术实践
- "Steal Like an Artist" 作者
- 创作过程与生活洞察
4. Paul Graham Essays (paulgraham.com)
- 创业哲学与黑客文化
- Y Combinator 创始人
- 经典长文(How to Do Great Work 等)
技术实现:
- 创建 4 个新 parser:
yuanchaofa.py,brainfood.py,austinkleon.py,paulgraham.py - 3 个使用 RSS feed(简单可靠),1 个 HTML 解析(Paul Graham 无 RSS)
- 更新
sources.json和fetch_updates.py - 测试通过,成功采集 45 篇文章(10+10+10+15)
内容矩阵扩展:
| 维度 | 原有源 | 新增源 |
|---|---|---|
| AI技术 | HN, HF, Import AI | 袁超发(深度实现) |
| 创业/产品 | PH, HN | Paul Graham(哲学) |
| 认知/思维 | James Clear, Wait But Why | Brain Food(决策) |
| 创意/艺术 | - | Austin Kleon(创作) |
设计哲学:
- 优先 RSS:3/4 使用 RSS,稳定可靠
- HTML 兜底:Paul Graham 无 RSS,简单 HTML 解析
- 零额外依赖:复用
web_utils.py的fetch_html()
v2.4 (2026-01-04) - Ben's Bites 解析器重构 ⚡
问题:
- Ben's Bites 采集失败,Playwright 访问主页超时(90秒)
- 过度工程化:用浏览器自动化对抗 Cloudflare,但 RSS feed 本身可直接访问
解决方案:
- 重写
parsers/bensbites.py,移除 Playwright 依赖 - 使用
web_utils.py的fetch_html()直接访问 RSS feed - 从 114 行减少到 107 行,零额外依赖
性能对比:
| 维度 | 修复前(Playwright) | 修复后(curl) |
|---|---|---|
| 速度 | 90秒超时,失败 | 2-3秒完成 |
| 依赖 | Playwright + Chromium | 零依赖 |
| 可靠性 | 容易因网站变化失败 | RSS 格式稳定 |
设计哲学:
- 简单优于复杂: Simple is better than complex
- 消除假想敌: RSS feed 是公开的,不需要对抗 Cloudflare
- 能消失的复杂性永远比能写对的复杂性更优雅
v2.3 (2026-01-04) - LLM 增强推荐 🤖
功能增强:
- 推荐引擎集成 LLM 能力,自动翻译标题并打分
- 三维评分体系:兴趣匹配度 + 深度潜力 + 写作可行性
- 生成乔木风格写作大纲(开头/核心观点/类比/结尾)
- 双文件输出:
recommendations.md(英文)+recommendations_enhanced.md(中文增强版)
设计哲学:
- 能用 LLM 直接解决的事,不要写脚本
- 简单、高效、零依赖
v2.2 (2026-01-03) - 路径配置化重构 🏗️
架构变更:
- 新增
config/paths.json- 统一输出路径配置 - 新增
scripts/shared/path_utils.py- 公共路径工具模块 - 重构所有脚本,移除硬编码路径,实现配置化
- 输出目录迁移: 从技能内部
data/移至用户知识库~/乔木新知识库/01.inbox/素材索引/
修改的文件:
fetch_updates.py- 使用get_content_log_path()recommend_topics.py- 使用get_recommendations_path()generate_weekly_index.py- 使用get_weekly_index_path(year, week)generate_starred_index.py- 使用get_starred_index_path()
设计哲学:
- 配置优于硬编码: 路径可配置,适应不同用户的知识库组织
- 单一真相源: 所有路径配置集中在
paths.json - 向后兼容: 若
paths.json不存在,回退到默认路径 - 关注点分离: 内部状态 (
state.json) 保留在技能目录,用户可见输出保存到知识库
文件组织哲学:
内部数据(state.json) → 技能目录 # 隐藏实现细节
用户输出(索引文件) → 知识库inbox # 用户可见可管理v2.0 (2025-12-27) - 智能推荐升级
架构变更:
- 新增
config/user_preferences.json- 用户偏好配置 - 重写
recommend_topics.py- 智能过滤+主题聚类+深度评分 - 新增 6大主题分类:AI模型/应用/政策/哲学/工程/创业
核心算法: 1. 兴趣过滤(减少66%噪音:112篇→38篇) 2. 深度评分(来源基础分+关键词加分) 3. 主题聚类(按关键词分组) 4. 乔木风格推荐模板(开头/核心观点/类比/结尾)
设计哲学:
- 从"通用推荐"到"个性化推荐"
- 信息采集≠知识筛选
- 数量不等于质量,好的推荐=过滤噪音+提供角度
v1.0 (2025-12-24) - 初始版本
基础功能:内容抓取、URL去重、简单推荐
---
📐 系统架构
qiaomu-ai-radar/
├── config/
│ ├── sources.json # 信息源配置(URL、parser、抓取间隔)
│ ├── user_preferences.json # 用户偏好配置(v2.0)
│ └── paths.json # 输出路径配置(v2.2)✨
├── scripts/
│ ├── fetch_updates.py # 核心抓取引擎(遍历 sources,调用 parsers)
│ ├── recommend_topics.py # 选题推荐引擎(分析素材,生成推荐)
│ ├── generate_weekly_index.py # 周刊索引生成器
│ ├── generate_starred_index.py # 收藏索引生成器
│ ├── parsers/ # 解析器库(每个信息源一个独立 parser)
│ │ ├── hackernews.py # [聚合器] HN 热门话题
│ │ ├── importai.py # [RSS] Import AI newsletter(AI 政策、研究)
│ │ ├── huggingface.py # [HTML] Hugging Face 社区热门论文
│ │ ├── jamesclear.py # [归档] James Clear 3-2-1 newsletter
│ │ ├── waitbutwhy.py # [博客] Wait But Why 深度长文
│ │ ├── producthunt.py # [聚合器] Product Hunt 新产品
│ │ ├── bensbites.py # [RSS] Ben's Bites(v2.4 修复,curl直接访问)
│ │ ├── tldrai.py # [RSS] TLDR AI(AI 行业快讯)
│ │ ├── yuanchaofa.py # [RSS] 袁超发技术博客(v2.5 新增)
│ │ ├── brainfood.py # [RSS] Farnam Street Brain Food(v2.5 新增)
│ │ ├── austinkleon.py # [RSS] Austin Kleon Substack(v2.5 新增)
│ │ ├── paulgraham.py # [HTML] Paul Graham Essays(v2.5 新增)
│ │ ├── lexfridman.py # [Podcast] Lex Fridman(v2.6 新增)🎙️
│ │ ├── cognitiverevolution.py # [Podcast] Cognitive Revolution(v2.6 新增)🎙️
│ │ ├── hours80k.py # [Podcast] 80,000 Hours(v2.6 新增)🎙️
│ │ └── latentspace.py # [Podcast] Latent Space(v2.6 新增)🎙️
│ └── shared/
│ ├── web_utils.py # 共享工具(curl wrapper,HTML 提取)
│ └── path_utils.py # 路径配置加载器(v2.2)✨
└── data/
├── state.json # URL 去重状态(MD5 hash,保留在技能目录)
├── auto_run.log # 自动运行日志
├── launchd.log # launchd 标准输出
└── launchd_error.log # launchd 错误输出
输出文件(v2.2 迁移到用户知识库):
~/乔木新知识库/01.inbox/素材索引/
├── content_log.md # 历史素材库(按日期组织)
├── recommendations.md # 最新推荐(英文版,recommend_topics.py 生成)
├── recommendations_enhanced.md # 智能推荐(中文翻译+打分+写作大纲,v2.3)
├── 2025-第1周.md # 周刊索引(按周组织)
├── 2025-第2周.md
└── ⭐️收藏精选.md # 收藏索引(跨周累积)---
🧠 设计哲学
1. 模块化解析器设计
问题: 不同网站结构千差万别,HTML/RSS/API 格式各异 方案: 每个信息源一个独立 parser,统一返回格式
# 所有 parser 返回统一结构
[
{
"url": "文章 URL",
"title": "标题",
"summary": "摘要(可选)"
}
]好处:
- 单一职责:每个 parser 只负责一个网站
- 易于扩展:添加新源 = 写新 parser + 注册
- 故障隔离:一个 parser 失败不影响其他
2. URL 去重的哲学
为什么用 MD5 hash 而非直接比对 URL?
# ❌ 坏方案:直接存储完整 URL
seen_urls = ["https://...", "https://..."] # 占用大量内存
# ✅ 好方案:存储 12 位 hash
seen_hashes = {"a8bbd636a456": "...", ...} # 紧凑高效权衡:
- Hash 冲突概率: MD5(12位) ≈ 1/16^12 ≈ 可忽略
- 内存节省: ~90%(完整 URL 通常 50+ 字符 → 12 字符)
- 查找速度: O(1) dict lookup
3. 为什么不用数据库?
设计决策: 文件系统 > SQLite > PostgreSQL
理由:
- 简单性: Markdown 可直接阅读,无需 SQL 查询
- 便携性: 整个 skill 是一个文件夹,可直接复制/同步
- 零依赖: 不需要安装数据库
- 够用就好: 即使每天抓取 100 篇文章 × 365 天 = 3.6 万条记录,Markdown 文件也就几 MB
何时需要数据库:
- 需要复杂查询(多维度筛选、全文搜索)
- 数据量 > 10 万条
- 需要并发写入
当前场景:过度工程 🚫
4. 选题推荐的启发式算法
策略: 基于规则的推荐 > 机器学习
# 推荐优先级
if source == "Import AI":
priority = "high" # 深度分析,适合写洞察
elif source == "Hugging Face Papers":
priority = "medium" # 论文解读
elif "replaced" in title or "simple" in title:
priority = "high" # 工程哲学类为什么不用 ML?
- 数据太少(每天 < 100 篇)
- 目标明确(找适合乔木风格的话题)
- 规则可解释、可调整
Linus 的品味: 简单规则 > 黑盒算法
---
🔧 关键设计决策
抓取频率
| 信息源 | 频率 | 理由 |
|---|---|---|
| Hacker News | 24h | 每天有新热点 |
| Import AI | 168h (周) | 每周发布 |
| Hugging Face | 24h | 论文更新频繁 |
| James Clear | 168h | 每周三发布 |
原则: 匹配内容更新频率,避免重复抓取
错误处理
哲学: Fail gracefully,永远交付结果
# ✅ 好的错误处理
for source in sources:
try:
articles = parser(source)
except Exception as e:
print(f"⚠️ {source} 失败: {e}")
continue # 跳过这个源,继续其他
# 即使所有 parser 都失败,也返回空列表而非崩溃案例: Ben's Bites 被 Cloudflare 拦截
- ❌ 糟糕方案:整个抓取失败
- ✅ 优雅方案:禁用该源,其他照常工作
---
📊 数据流
[定时任务 9:00 AM]
↓
fetch_updates.py
↓
遍历 sources.json
↓
调用各个 parser ──→ [网络请求] ──→ HTML/RSS/JSON
↓
提取文章 {url, title, summary}
↓
URL 去重 (hash 比对 state.json)
↓
追加到 content_log.md
↓
更新 state.json
↓
[用户调用 skill]
↓
recommend_topics.py
↓
读取 content_log.md(最近3天)
↓
分析主题、来源、关键词
↓
生成推荐 → recommendations.md
↓
返回给用户---
🚀 扩展指南
添加新信息源(3 步)
Step 1: 写 parser
# parsers/newsource.py
def parse_newsource(source):
html = fetch_html(source["url"])
# 提取文章...
return [{"url": ..., "title": ..., "summary": ...}]Step 2: 注册 parser
# fetch_updates.py
from newsource import parse_newsource
PARSERS = {
# ...
"newsource": parse_newsource,
}Step 3: 添加配置
// config/sources.json
{
"id": "newsource",
"name": "New Source",
"url": "https://...",
"parser": "newsource",
"enabled": true,
"check_interval_hours": 24
}就是这么简单 💪
---
📈 性能特征
- 抓取速度: ~5 秒/源(网络延迟为主)
- 内存占用: < 50 MB(纯 Python,无数据库)
- 存储增长: ~10 KB/天(假设 50 篇文章 × 200 字符)
- 可扩展性: 单机可支持 100+ 信息源
瓶颈: 网络 I/O,非计算或存储
---
⚠️ 已知限制
1. Cloudflare 防护
- 影响: Ben's Bites、部分 Substack 站点
- 方案: 改用 RSS feed 或 Playwright
2. HTML 结构变化
- 风险: 网站改版导致 parser 失效
- 缓解: 定期检查 cron.error.log
3. 推荐算法简陋
- 现状: 基于规则的启发式
- 改进: 可引入 TF-IDF、主题建模
4. 无全文搜索
- 现状: 只能按日期浏览
- 改进: 可添加 SQLite FTS5
---
🎯 设计权衡总结
| 决策 | 选择 | 权衡 |
|---|---|---|
| 存储 | Markdown 文件 | 简单 vs 查询能力 |
| 去重 | MD5 hash | 内存效率 vs 极小冲突风险 |
| 推荐 | 基于规则 | 可解释 vs 智能程度 |
| 错误处理 | Fail gracefully | 可靠性 > 完美性 |
| Parser 架构 | 独立模块 | 可维护 > 代码复用 |
核心哲学: 实用主义 > 完美主义(Linus Good Taste ✅)
---
📝 维护清单
每周检查
- [ ]
cron.error.log有无新错误 - [ ]
content_log.md文件大小(建议 < 10 MB)
每月检查
- [ ] 各 parser 是否仍工作(网站结构是否变化)
- [ ]
state.json大小(建议 < 5 MB,可清理 90 天前记录)
按需优化
- [ ] 添加新信息源
- [ ] 优化推荐算法
- [ ] 清理历史数据(保留最近 1 年)
---
哲学总结: 这个 skill 的设计遵循 Linus 的 Good Taste 原则——消除特殊情况,让边界自然融入常规。每个 parser 独立,每个错误被隔离,整个系统即使部分失效也能继续工作。简单、可靠、易扩展。
Good code doesn't need exceptions. Good architecture doesn't need rescue.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.qiaomu-ai-radar</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/YOUR_USERNAME/.agents/skills/qiaomu-ai-radar/scripts/run_radar.py</string>
<string>--mode</string>
<string>fetch</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/YOUR_USERNAME/.local/node/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/.agents/skills/qiaomu-ai-radar/data/cron.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/.agents/skills/qiaomu-ai-radar/data/cron.error.log</string>
</dict>
</plist>
{
"blacklist": {
"keywords": {
"title": [
"lua",
"lisp",
"haskell",
"rust compiler",
"UEFI",
"GPIB",
"FPGA",
"HBM",
"scheduler",
"garbage collection",
"TCP_NODELAY",
"HTTP caching",
"PostgreSQL",
"NixOS",
"LoongArch",
"LSP support",
"fined",
"law blocked",
"spying",
"privacy",
"router",
"font",
"typeface",
"runway design",
"billiards",
"μs off",
"redactions",
"charts in slides",
"lockfiles",
"SBOM",
"terminal emulator",
"ghostty",
"wezterm",
"alacritty",
"AI safety",
"AI governance",
"AI regulation",
"situational awareness",
"effective altruism",
"executive briefing",
"corporate AI adoption",
"VC funding round",
"series A funding",
"series B funding",
"fundraising round"
],
"url": [
"twitter.com",
"x.com"
],
"summary": [
"sponsor"
]
},
"sources": {
"Wait But Why": {
"exclude_url_patterns": [
"#comments$"
]
}
}
},
"whitelist": {
"keywords": {
"title": [
"AI",
"LLM",
"GPT",
"Claude",
"transformer",
"machine learning",
"engineering blog",
"architecture",
"design philosophy",
"bootstrapped",
"startup",
"senior engineer",
"coding",
"software design",
"MCP",
"tool use",
"WebMCP",
"AGENTS.md",
"from scratch",
"interactive",
"explainer",
"SVG",
"visualization"
]
}
},
"rules": {
"apply_order": "blacklist_first",
"case_sensitive": false,
"comment": "黑名单优先:先过滤黑名单,再检查白名单"
}
}
{
"version": "1.0",
"description": "qiaomu-ai-radar 输出路径配置 - 所有用户可见文件统一保存到知识库",
"_comment_output_base": "基础输出目录 - 所有生成的文件都保存到此处",
"output_base_dir": "~/乔木新知识库/20-29 学习/21 素材索引",
"_comment_files": "日常生成的文件名",
"files": {
"content_log": "content_log.md",
"recommendations": "recommendations.md"
},
"_comment_index": "索引文件的命名模板",
"index_files": {
"weekly_template": "{year}-第{week}周.md",
"starred": "⭐️收藏精选.md"
},
"_comment_internal": "内部状态文件仍保留在技能目录",
"internal_files": {
"state": "data/state.json",
"database": "data/articles.db"
},
"_comment_feishu": "飞书同步配置 - 把本周索引镜像到云空间目录",
"feishu": {
"enabled": true,
"cli": "/opt/homebrew/bin/lark-cli",
"identity": "user",
"folder_name": "乔木AI雷达",
"parent_folder_token": "",
"state_file": "data/feishu_sync.json"
}
}
{
"version": "1.0",
"description": "渠道偏好配置 - 喜欢的渠道排在前面,带⭐标记",
"favorite_sources": [
"NLP Newsletter (Elvis Saravia)"
],
"muted_sources": [],
"_comment": "favorite_sources: 偏好渠道名(对应 sources.json 中的 name 字段),排在前面且带⭐标记。muted_sources: 静音渠道,采集但折叠显示。"
}
{
"sources": [
{
"id": "james-clear",
"name": "James Clear 3-2-1",
"url": "https://jamesclear.com/3-2-1",
"type": "newsletter_archive",
"parser": "jamesclear",
"enabled": true,
"check_interval_hours": 168,
"last_checked": "2026-06-02T01:00:19.870362+00:00"
},
{
"id": "bensbites",
"name": "Ben's Bites",
"url": "https://www.bensbites.com/archive",
"type": "newsletter_archive",
"parser": "bensbites",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870237+00:00"
},
{
"id": "tldrai",
"name": "TLDR AI",
"url": "https://tldr.tech/api/latest/ai",
"type": "newsletter",
"parser": "tldrai",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870440+00:00"
},
{
"id": "importai",
"name": "Import AI",
"url": "https://importai.substack.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870123+00:00"
},
{
"id": "nlp-elvis",
"name": "NLP Newsletter (Elvis Saravia)",
"url": "https://nlp.elvissaravia.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"last_checked": "2026-06-02T01:00:19.870359+00:00"
},
{
"id": "interconnects",
"name": "Interconnects (Nathan Lambert)",
"url": "https://www.interconnects.ai/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 24,
"limit": 5,
"last_checked": "2026-06-02T01:00:19.870343+00:00"
},
{
"id": "huggingface",
"name": "Hugging Face Papers",
"url": "https://huggingface.co/papers",
"type": "aggregator",
"parser": "huggingface",
"enabled": true,
"check_interval_hours": 24,
"limit": 15,
"min_votes": 30,
"last_checked": "2026-06-02T01:00:19.870312+00:00"
},
{
"id": "waitbutwhy",
"name": "Wait But Why",
"url": "https://waitbutwhy.com",
"type": "blog",
"parser": "waitbutwhy",
"enabled": true,
"check_interval_hours": 168,
"last_checked": "2026-06-02T01:00:19.869956+00:00"
},
{
"id": "hackernews",
"name": "Hacker News",
"url": "https://news.ycombinator.com/",
"type": "aggregator",
"parser": "hackernews",
"enabled": false,
"_note": "用户要求禁用,信息噪音太大",
"check_interval_hours": 24,
"limit": 8,
"last_checked": "2026-04-18T08:45:50.442522+00:00"
},
{
"id": "producthunt",
"name": "Product Hunt",
"url": "https://www.producthunt.com",
"type": "aggregator",
"parser": "producthunt",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870368+00:00"
},
{
"id": "yuanchaofa",
"name": "袁超发技术博客",
"url": "https://yuanchaofa.com/rss.xml",
"type": "blog",
"parser": "yuanchaofa",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870481+00:00"
},
{
"id": "brainfood",
"name": "Farnam Street Brain Food",
"url": "https://fs.blog/brain-food/",
"type": "newsletter_rss",
"parser": "brainfood",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870514+00:00"
},
{
"id": "austinkleon",
"name": "Austin Kleon",
"url": "https://austinkleon.substack.com/archive",
"type": "newsletter_rss",
"parser": "austinkleon",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.870584+00:00"
},
{
"id": "paulgraham",
"name": "Paul Graham Essays",
"url": "https://paulgraham.com/articles.html",
"type": "blog",
"parser": "paulgraham",
"enabled": true,
"check_interval_hours": 168,
"limit": 15,
"last_checked": "2026-06-02T01:00:19.871368+00:00"
},
{
"id": "lexfridman",
"name": "Lex Fridman Podcast",
"url": "https://lexfridman.com/feed/podcast/",
"type": "podcast",
"parser": "lexfridman",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871914+00:00"
},
{
"id": "cognitiverevolution",
"name": "Cognitive Revolution",
"url": "https://feeds.megaphone.fm/RINTP3108857801",
"type": "podcast",
"parser": "cognitiverevolution",
"enabled": false,
"_note": "用户明确表示不感兴趣(AI safety/policy方向)",
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-03-02T03:15:28.938078+00:00"
},
{
"id": "hours80k",
"name": "80,000 Hours Podcast",
"url": "https://feeds.transistor.fm/80000-hours-podcast",
"type": "podcast",
"parser": "hours80k",
"enabled": false,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-03-02T03:15:31.617653+00:00"
},
{
"id": "latentspace",
"name": "Latent Space Podcast",
"url": "https://www.latent.space/feed",
"type": "podcast",
"parser": "latentspace",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871242+00:00"
},
{
"id": "oneusefulthing",
"name": "One Useful Thing (Ethan Mollick)",
"url": "https://www.oneusefulthing.org/feed",
"type": "newsletter_rss",
"parser": "oneusefulthing",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871296+00:00"
},
{
"id": "hackernoon-lifehacking",
"name": "HackerNoon Life Hacking",
"url": "https://hackernoon.com/c/life-hacking",
"type": "aggregator",
"parser": "hackernoon",
"enabled": false,
"_note": "16篇0阅读",
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-03-02T03:15:38.332150+00:00"
},
{
"id": "hackernoon-writing",
"name": "HackerNoon Writing",
"url": "https://hackernoon.com/c/writing",
"type": "aggregator",
"parser": "hackernoon",
"enabled": false,
"_note": "18篇0阅读",
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-03-02T03:15:40.544115+00:00"
},
{
"id": "hackernoon-pm",
"name": "HackerNoon Product Management",
"url": "https://hackernoon.com/c/product-management",
"type": "aggregator",
"parser": "hackernoon",
"enabled": false,
"_note": "15篇0阅读",
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-03-02T03:15:42.784834+00:00"
},
{
"id": "kdnuggets-ai",
"name": "KDnuggets AI",
"url": "https://www.kdnuggets.com/tag/artificial-intelligence",
"type": "aggregator",
"parser": "kdnuggets",
"enabled": false,
"_note": "92篇0阅读,纯噪音",
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-03-02T03:15:46.141494+00:00"
},
{
"id": "scotthyoung",
"name": "Scott H Young",
"url": "https://www.scotthyoung.com/blog/articles/",
"type": "blog",
"parser": "scotthyoung",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871432+00:00"
},
{
"id": "readwise-wise",
"name": "Readwise Wise",
"url": "https://wise.readwise.io/",
"type": "newsletter",
"parser": "readwise",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.872010+00:00"
},
{
"id": "dcthemedian",
"name": "DC The Median",
"url": "https://dcthemedian.substack.com/",
"type": "newsletter_rss",
"parser": "dcthemedian",
"enabled": false,
"_note": "10篇0阅读",
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-03-02T03:15:55.657762+00:00"
},
{
"id": "markmcneilly",
"name": "Mark McNeilly",
"url": "https://markmcneilly.substack.com/",
"type": "newsletter_rss",
"parser": "markmcneilly",
"enabled": false,
"_note": "12篇0阅读",
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-03-02T03:15:58.226235+00:00"
},
{
"id": "businessanalytics",
"name": "Business Analytics",
"url": "https://businessanalytics.substack.com/",
"type": "newsletter_rss",
"parser": "businessanalytics",
"enabled": false,
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-01-11T12:15:01.346047+00:00"
},
{
"id": "whytryai",
"name": "Why Try AI",
"url": "https://www.whytryai.com/",
"type": "newsletter_rss",
"parser": "whytryai",
"enabled": true,
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-06-02T01:00:19.871409+00:00"
},
{
"id": "therundown",
"name": "The Rundown AI",
"url": "https://rss.beehiiv.com/feeds/2R3C6Bt5wj.xml",
"type": "newsletter_rss",
"parser": "therundown",
"enabled": false,
"_note": "39篇0阅读,与TLDR AI重复覆盖",
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-03-02T03:16:02.943836+00:00"
},
{
"id": "theneuron",
"name": "The Neuron Daily",
"url": "https://rss.beehiiv.com/feeds/N4eCstxvgX.xml",
"type": "newsletter_rss",
"parser": "theneuron",
"enabled": false,
"_note": "59篇0阅读,与TLDR AI/Rundown AI重复",
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-03-02T03:16:05.367977+00:00"
},
{
"id": "aileadershipedge",
"name": "AI Leadership Edge",
"url": "https://theaileadershipedge.substack.com/",
"type": "newsletter_rss",
"parser": "aileadershipedge",
"enabled": false,
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-03-02T03:16:07.549035+00:00"
},
{
"id": "chinai",
"name": "ChinAI Newsletter",
"url": "https://chinai.substack.com/",
"type": "newsletter_rss",
"parser": "chinai",
"enabled": true,
"check_interval_hours": 168,
"limit": 3,
"last_checked": "2026-06-02T01:00:19.871336+00:00"
},
{
"id": "memia",
"name": "Memia (Ben Reid)",
"url": "https://memia.substack.com/",
"type": "newsletter",
"parser": "memia",
"enabled": false,
"_note": "15篇0阅读",
"check_interval_hours": 168,
"limit": 5,
"last_checked": "2026-03-02T03:16:12.172777+00:00"
},
{
"id": "ai2roi",
"name": "AI to ROI",
"url": "https://ai2roi.substack.com/",
"type": "newsletter",
"parser": "ai2roi",
"enabled": false,
"check_interval_hours": 24,
"limit": 5,
"last_checked": "2026-03-02T03:16:14.612613+00:00"
},
{
"id": "natesnewsletter",
"name": "Nate's Newsletter",
"url": "https://natesnewsletter.substack.com/",
"type": "newsletter",
"parser": "natesnewsletter",
"enabled": false,
"_note": "企业AI战略方向,用户明确不感兴趣",
"check_interval_hours": 24,
"limit": 5,
"last_checked": "2026-03-02T03:16:16.438555+00:00"
},
{
"id": "aichangeseverything",
"name": "AI Changes Everything",
"url": "https://patmcguinness.substack.com/",
"type": "newsletter",
"parser": "aichangeseverything",
"enabled": false,
"_note": "12篇0阅读",
"check_interval_hours": 24,
"limit": 5,
"last_checked": "2026-03-02T03:16:18.571802+00:00"
},
{
"id": "dankoe",
"name": "Dan Koe Letters",
"url": "https://thedankoe.com/letters/",
"type": "newsletter_archive",
"parser": "dankoe",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871474+00:00"
},
{
"id": "shewritesai",
"name": "She Writes AI",
"url": "https://shewritesai.substack.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": false,
"_note": "11篇0阅读",
"check_interval_hours": 24,
"limit": 5,
"description": "AI writing and content creation insights",
"last_checked": "2026-03-02T03:16:23.724089+00:00"
},
{
"id": "lolitataub",
"name": "Lolita Taub",
"url": "https://lolitataub.substack.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": false,
"_note": "5篇0阅读,VC方向不匹配",
"check_interval_hours": 24,
"limit": 5,
"description": "VC and startup insights",
"last_checked": "2026-03-02T03:16:25.800242+00:00"
},
{
"id": "aitalks",
"name": "AI Talks",
"url": "https://aitalks.blog/feed/",
"type": "newsletter_rss",
"parser": "importai",
"enabled": false,
"_note": "7篇0阅读",
"check_interval_hours": 24,
"limit": 5,
"description": "AI revolution navigation and weekly news",
"last_checked": "2026-03-02T03:16:27.391858+00:00"
},
{
"id": "kexuefm",
"name": "科学空间 (Scientific Spaces)",
"url": "https://kexue.fm/feed",
"type": "blog_rss",
"parser": "kexuefm",
"enabled": true,
"check_interval_hours": 168,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871397+00:00",
"description": "苏剑林的AI/ML技术博客,MoE环游记系列"
},
{
"id": "tylerfolkman",
"name": "Tyler Folkman (AI Newsletter)",
"url": "https://tylerfolkman.substack.com/",
"type": "newsletter_rss",
"parser": "tylerfolkman",
"enabled": true,
"check_interval_hours": 24,
"limit": 3,
"last_checked": "2026-06-02T01:00:19.871910+00:00"
},
{
"id": "github_trending",
"name": "GitHub Trending",
"url": "https://github.com/trending",
"type": "aggregator",
"parser": "github_trending",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"last_checked": "2026-06-02T01:00:19.871899+00:00"
},
{
"id": "superhuman_ai",
"name": "Superhuman AI",
"url": "https://www.superhuman.ai",
"type": "newsletter_sitemap",
"parser": "superhuman_ai",
"enabled": true,
"check_interval_hours": 24,
"limit": 3,
"description": "AI工具与技术进展日报",
"last_checked": "2026-06-02T01:00:19.871952+00:00"
},
{
"id": "aibreakfast",
"name": "AI Breakfast",
"url": "https://aibreakfast.beehiiv.com",
"type": "newsletter_sitemap",
"parser": "aibreakfast",
"enabled": true,
"check_interval_hours": 168,
"limit": 3,
"description": "每周AI新闻与工具精选",
"last_checked": "2026-06-02T01:00:19.872013+00:00"
},
{
"id": "reddit_ai",
"name": "Reddit AI (ML+LocalLLaMA+ClaudeAI+Anthropic)",
"url": "https://www.reddit.com/r/MachineLearning+LocalLLaMA+ClaudeAI+Anthropic/.rss",
"type": "aggregator_rss",
"parser": "reddit_ai",
"enabled": false,
"_note": "用户要求禁用",
"check_interval_hours": 24,
"limit": 8,
"description": "Reddit AI社区热帖:机器学习、本地LLM、Claude、Anthropic",
"last_checked": "2026-04-18T08:45:50.442890+00:00"
},
{
"id": "bestblogs",
"name": "BestBlogs.dev",
"url": "https://www.bestblogs.dev/articles",
"type": "aggregator_html",
"parser": "bestblogs",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"description": "中文技术博客聚合:AI、编程、产品、商业精选",
"last_checked": "2026-06-02T01:00:19.872041+00:00"
},
{
"id": "simonwillison",
"name": "Simon Willison's Weblog",
"url": "https://simonwillison.net/atom/everything/",
"type": "blog_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"description": "LLM/AI工具最活跃独立博主,Django联创,频繁评测Claude/GPT",
"last_checked": "2026-06-02T01:00:19.871916+00:00"
},
{
"id": "garymarcus",
"name": "Gary Marcus (The Road to AI We Can Trust)",
"url": "https://garymarcus.substack.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "AI批评视角,提供平衡观点素材",
"last_checked": "2026-06-02T01:00:19.871930+00:00"
},
{
"id": "dwarkesh",
"name": "Dwarkesh Patel",
"url": "https://www.dwarkeshpatel.com/feed",
"type": "blog_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "深度访谈AI领袖和技术思想家",
"last_checked": "2026-06-02T01:00:19.872026+00:00"
},
{
"id": "gwern",
"name": "Gwern Branwen",
"url": "https://gwern.substack.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "AI/未来主义/深度长文思考",
"last_checked": "2026-06-02T01:00:19.871943+00:00"
},
{
"id": "geoffreylitt",
"name": "Geoffrey Litt",
"url": "https://www.geoffreylitt.com/feed.xml",
"type": "blog_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "AI/HCI研究,工具设计思考",
"last_checked": "2026-06-02T01:00:19.871983+00:00"
},
{
"id": "experimental-history",
"name": "Experimental History",
"url": "https://www.experimental-history.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "社会科学实验视角,独特洞察",
"last_checked": "2026-06-02T01:00:19.871967+00:00"
},
{
"id": "construction-physics",
"name": "Construction Physics",
"url": "https://www.construction-physics.com/feed",
"type": "newsletter_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "工程进步与科技发展分析",
"last_checked": "2026-06-02T01:00:19.871980+00:00"
},
{
"id": "antirez",
"name": "Salvatore Sanfilippo (antirez)",
"url": "http://antirez.com/rss",
"type": "blog_rss",
"parser": "importai",
"enabled": true,
"check_interval_hours": 168,
"limit": 5,
"description": "Redis作者博客,技术洞察",
"last_checked": "2026-06-02T01:00:19.872039+00:00"
},
{
"id": "theresanaiforthat",
"name": "There's An AI For That",
"url": "https://newsletter.theresanaiforthat.com",
"type": "newsletter_sitemap",
"parser": "beehiiv_sitemap",
"enabled": true,
"check_interval_hours": 168,
"limit": 3,
"description": "AI工具发现平台官方通讯,每周精选AI工具推荐",
"last_checked": "2026-06-02T01:00:19.871986+00:00"
}
]
}{
"_meta": {
"version": "2.2",
"updated_at": "2026-03-02",
"updated_by": "基于全量阅读数据 + 第10周反馈优化(2500+篇行为分析)"
},
"user_profile": {
"name": "Joe",
"writing_style": "qiaomu",
"primary_interests": [
"AI Research Papers & Surveys",
"AI Agents & MCP/Tool Use",
"AI Reasoning & Thinking",
"Interactive Technical Explainers",
"Practical AI Tools & Reviews",
"Show HN Projects",
"Programming Craft & Engineering Philosophy",
"Web Standards & Browser APIs",
"Human Psychology & Social Insights"
],
"secondary_interests": [
"AI Video/Vision Generation",
"Multimodal AI",
"Diffusion Models",
"SVG/Design Engineering",
"Career & Founder Stories",
"Open Source Culture",
"Creative Tools",
"Performance Engineering"
],
"excluded_topics": [
"DevOps Infrastructure",
"Cloud Platform Admin",
"Enterprise SaaS Marketing",
"Crypto/Web3 Speculation",
"Political News",
"Sports",
"AI Safety & Governance Policy",
"AI Ethics & Regulation Debates",
"Enterprise AI Strategy & Management",
"Executive Briefings & Corporate AI Adoption",
"Terminal Emulators & Shell Tools",
"Effective Altruism",
"VC/Investment Analysis"
]
},
"recommendation_preferences": {
"min_articles_per_topic": 2,
"max_recommendations": 10,
"preferred_sources": [
"Hacker News",
"Hugging Face Papers",
"NLP Newsletter (Elvis Saravia)",
"Readwise Wise",
"Import AI",
"Interconnects (Nathan Lambert)",
"科学空间 (Scientific Spaces)",
"Ben's Bites",
"TLDR AI",
"Latent Space Podcast",
"Paul Graham Essays",
"Wait But Why",
"Why Try AI",
"One Useful Thing (Ethan Mollick)"
],
"scoring_weights": {
"interest_match": 0.45,
"depth_potential": 0.25,
"timeliness": 0.15,
"source_reputation": 0.15
}
},
"content_filters": {
"high_priority_keywords": [
"Video Generation", "Video Understanding", "4D", "3D Scene",
"Vision Language Model", "Multimodal", "Visual",
"World Model", "Agent", "Agentic", "Autonomous",
"Reasoning", "Thinking", "Test-Time", "Chain of Thought",
"Diffusion", "Transformer", "Attention",
"Show HN", "I built", "I made",
"Interactive", "Explainer", "Playground", "Visualiz",
"Top AI Papers", "Survey", "Benchmark",
"SVG", "Design Tool", "Arrow", "Diagram",
"Compiler", "Algorithm", "Data Structure",
"MCP", "Tool Use", "Function Calling", "WebMCP",
"AGENTS.md", "CLAUDE.md", "Context Protocol",
"from scratch", "in 200 lines", "minimal implementation"
],
"medium_priority_keywords": [
"LLM", "GPT", "Claude", "ChatGPT", "OpenAI", "Anthropic",
"Training", "Fine-tuning", "RLHF", "RLVR", "Reinforcement",
"Image Generation", "Audio", "TTS", "Speech",
"Embedding", "RAG", "Context Window",
"Open Source", "GitHub",
"Founder", "Startup", "CEO", "Career",
"Linux", "Kernel", "Systems Programming",
"Performance", "Optimization", "Fast", "Speed",
"Psychology", "Social", "Behavior", "Human"
],
"exclude_keywords": [
"Kubernetes", "Docker Swarm", "Terraform", "Ansible",
"Enterprise", "B2B SaaS Marketing",
"Crypto", "Bitcoin", "Blockchain", "NFT", "Web3",
"Political", "Election", "Government Policy",
"AI Safety", "AI Governance", "AI Regulation", "AI Ethics",
"Situational Awareness", "Effective Altruism",
"Executive Briefing", "Corporate AI Adoption",
"Terminal Emulator", "Ghostty", "Wezterm", "Alacritty",
"VC Funding", "Series A", "Series B", "Fundraising Round"
],
"source_specific_filters": {
"Hugging Face Papers": {
"prefer": ["Video", "Vision", "Agent", "World Model", "Reasoning", "Diffusion", "Tool Use"],
"min_votes": 20
},
"Hacker News": {
"prefer": ["Show HN", "AI", "Interactive", "Programming", "Performance", "MCP", "from scratch"],
"avoid": ["political", "sports", "celebrity", "terminal emulator", "ghostty"]
}
}
},
"writing_goals": {
"article_types": [
"深度技术解读(2000-3000字)- 论文/系统设计",
"工具体验分享(1000-1500字)- Show HN 项目",
"认知升级(1500-2000字)- 方法论/思维方式",
"行业观察(1000-1500字)- 趋势/人物"
],
"target_audience": "对AI和编程感兴趣、有一定技术背景的读者",
"tone": "conversational, insightful, technically grounded"
},
"discovered_patterns": {
"reading_behavior": {
"total_articles": 2500,
"read_rate": "10%",
"star_rate_when_read": "88% (HN), 100% (HF)",
"primary_reading_sources": "HN (172 read) + HF (59 read) = 96% of all reads",
"never_read_sources": "KDnuggets, HackerNoon (all 3), Neuron Daily, Rundown AI, 多个substack"
},
"week10_feedback": {
"excited_about": ["AI Papers周报", "MicroGPT交互讲解", "QuiverAI Arrow SVG", "WebMCP标准"],
"followed_with_interest": ["MCP vs CLI辩论", "AGENTS.md研究", "PAHF个性化Agent"],
"not_interested": ["Cognitive Revolution (AI policy)", "Nate's Newsletter (enterprise)", "Ghostty (terminal)"]
},
"top_hf_categories": [
"视觉/视频生成 (29%)",
"Agent/世界模型 (15%)",
"扩散/生成 (14%)",
"推理/思考 (14%)"
],
"top_hn_categories": [
"技术深度文章 (62%)",
"AI相关 (13%)",
"Linux/系统 (7%)",
"Show HN 项目 (7%)"
],
"source_preference": {
"Hacker News": "71% of starred",
"Hugging Face Papers": "28% of starred"
}
}
}
每日选题助手
⚠️ 强制执行规则
每次调用此 skill 时,无论用户说什么,都必须先执行模式 A(抓取更新 + 自动翻译 + 飞书同步)。
执行顺序: 1. 先执行模式 A:抓取最新内容 + 翻译 + 同步飞书 + 打开 Obsidian 2. 再根据用户意图:如果用户明确要求推荐选题,继续执行模式 B
不要询问用户是否需要抓取,直接执行。
版本历史
v2.18 (2026-05-20) - 飞书本周索引同步
目标:Obsidian 本地周刊仍作为源文件,同时在飞书云空间维护一份可共享的本周索引。
工作流升级: 1. fetch_updates.py 每次运行都会确保本周 Markdown 索引存在。 2. 通过 lark-cli 确保飞书云空间中存在 乔木AI雷达 文件夹。 3. 首次同步本周索引时创建飞书云文档;同一周后续抓取覆盖更新同一份文档。 4. Claude 完成标题翻译后必须再次运行 sync_feishu_weekly.py,确保飞书内容也是翻译后的最终版本。
状态文件:data/feishu_sync.json 记录飞书目录 token 和每周文档 token,避免重复创建。
v2.17 (2026-04-14) - 强制验证机制 ⚠️
问题:Claude 会"幻觉"式地声称完成翻译,但实际没有调用任何 Edit 工具,导致文件内容完全没变。
根因分析:
- Claude 容易产生"我做了X"的幻觉,但实际工具调用记录为空
- 用户看到文件没变化,立即发现问题:"确定你翻译了吗?我看文件里并没有"
- 这种错误会严重损害用户信任
解决方案: 1. 强制自检机制:
- 在说"翻译完成"之前,必须检查工具调用记录
- 必须看到
<invoke name="Edit">和<output>...updated successfully</output> - 如果没有这些输出,说明只是"想象"了翻译
2. 验证式报告:
- 报告格式:
✅ 翻译完成!实际执行了 X 次 Edit 操作 - 从工具返回结果中统计成功的 Edit 数量
- 禁止在没有实际 Edit 调用时说"翻译完成"
3. 明确检查点:
- ⚠️ 关键:必须看到工具调用输出
- ⚠️ 关键:必须统计成功次数
- ⚠️ 关键:报告必须基于实际执行结果
设计哲学:
- 实事求是:只报告实际做了的事,不报告想象的事
- 可验证性:用户可以通过文件变化验证 AI 的声明
- 信任建立:准确的报告比虚假的"完成"更重要
效果:
- 改进前:说"翻译完成"但文件没变,用户立即发现问题
- 改进后:基于实际工具调用报告,用户看到文件确实改变了
v2.16 (2026-03-24) - 精准 Edit 模式
问题:v2.15 的 Read→Write 方案在大文件时仍然慢(输出 700+ 行)。
解决方案:每个未翻译标题单独一次 Edit,并行发出所有调用。
效果:
- Write 整个文件(700+ 行)= 输出 700+ 行内容
- 15 次小 Edit = 只输出修改的部分,更快
v2.15 (2026-03-04) - Read→Write 极速过滤翻译 ⚡
问题:v2.14 后台 agent 虽不阻塞主线程,但仍用逐条 Edit 处理(50+ 次 API 调用,~5 分钟)。
根因:每条删除/翻译都是一次 Edit API 调用,43 条翻译 + 10 条删除 = 53 次往返。
解决方案:Read 一次 → 内存中完成全部处理 → Write 一次 1. Read 读取整个周刊文件(1 次调用) 2. 内存处理:agent 在内存中同时完成过滤(删除不相关条目)+ 翻译(添加中文标题) 3. Write 将处理后的完整内容一次性写回(1 次调用) 4. 总计 2 次工具调用,替代原来的 50+ 次
效果:
- 改进前:50+ 次 Edit 调用,~5 分钟,89K tokens
- 改进后:2 次调用(Read + Write),~10 秒
设计哲学:
- 批量 > 逐条:能一次搞定的事不要分 50 次做
- 内存处理最快:LLM 在内存中处理文本的速度远超工具调用开销
- 保持后台执行:仍用 Task agent 后台运行,不阻塞主线程
v2.14 (2026-02-12) - Agent 后台处理过滤+翻译 🚀
问题:过滤和翻译在主线程逐条 Edit 执行,速度慢,占用对话上下文。
解决方案: 1. 抓取脚本在主线程执行(需要看输出判断结果) 2. 过滤+翻译用 Task agent 后台执行:
- 使用
Task工具,run_in_background: true,subagent_type: general-purpose - 一次性把过滤规则和翻译规范都写进 prompt
- agent 在后台独立完成所有 Edit 操作
3. 等 agent 完成后,打开 Obsidian + 汇报结果
设计哲学:
- 不阻塞主线程:用户可以继续聊天,后台默默处理
- 批量处理更高效:agent 一次性处理所有条目,不用来回切换
- 上下文更干净:主对话不被大量 Edit 操作污染
效果:
- 改进前:主线程逐条过滤+翻译,慢且占用上下文
- 改进后:后台 agent 一次性处理,快且干净
v2.13 (2026-02-06) - 自动打开 Obsidian 📖
问题:抓取和翻译完成后,Claude没有自动打开 Obsidian,用户需要手动打开文件阅读。
解决方案: 1. 新增第4步:翻译完成后强制执行 open "obsidian://..." 命令 2. 零摩擦阅读:用户触发抓取后,直接在 Obsidian 中看到翻译好的周刊
设计哲学:
- 完整闭环:抓取 → 翻译 → 打开 Obsidian,一气呵成
- 减少手动操作:用户只需触发一次,后续全自动
效果:
- 改进前:抓取完成 → 翻译完成 → 报告结束(用户需手动打开 Obsidian)
- 改进后:抓取完成 → 翻译完成 → 自动打开 Obsidian → 直接阅读
v2.12 (2026-01-12) - 全量翻译修复 🌐
问题:翻译只处理新增条目,历史英文标题被遗漏,导致周刊中大量英文标题未翻译。
根因分析:
- 脚本输出"新增条目数: 2",Claude误解为只翻译这2条
- SKILL.md说"找到所有纯英文标题",但没有明确定义"所有"的范围
解决方案: 1. 明确翻译范围:翻译周刊中所有未翻译的英文标题,不仅是新增的 2. 定义"未翻译":标题不包含" / "分隔符的都是未翻译 3. 检测逻辑:
- ✅ 已翻译:
**标题**: English Title / 中文翻译 - ❌ 未翻译:
**标题**: English Title(不含" / ")
设计哲学:
- 消除歧义:明确定义什么需要翻译,而非依赖Claude推断
- 全量处理:每次抓取后扫描整个周刊,确保无遗漏
- 幂等性:已翻译的不会重复处理
效果:
- 改进前:抓取2篇 → 只翻译2篇 → 历史50篇英文标题被忽略
- 改进后:抓取2篇 → 扫描全文 → 翻译所有52篇未翻译标题
v2.11 (2026-01-11) - 强制翻译执行机制 🚨
问题:抓取完成后Claude经常忘记执行翻译步骤,导致周刊中大量英文标题未翻译。
解决方案: 1. 脚本输出强制指令:
fetch_updates.py末尾输出CLAUDE_ACTION_REQUIRED: TRANSLATE_TITLES- 明确显示待翻译文件路径和条目数
- 输出完整的翻译执行步骤
2. SKILL.md 强调:
- 模式A流程中标注 🚨 强制翻译
- 明确说明"不可跳过"
- 增加警告提示
设计哲学:
- 显式优于隐式:用明确的指令触发行为,而非依赖Claude"记得"
- 机器可读:
CLAUDE_ACTION_REQUIRED作为标准触发词 - 无法忽略:醒目的分隔线和大写关键词
效果:
- 改进前:抓取13篇 → 报告数量 → 忘记翻译
- 改进后:抓取13篇 → 看到强制指令 → 立即执行翻译
v2.10 (2026-01-10) - AI相关性智能过滤 🧹
问题:Hacker News等综合平台"全吞"模式,大量与AI无关的内容进入周刊:
- 铁路新闻(Caltrain区域铁路)
- 游戏硬件(RTX 5090配树莓派)
- 政治新闻、体育赛事等
解决方案:延续v2.7设计哲学——脚本负责采集,Claude负责智能
工作流升级:
采集 → 智能过滤(新增)→ 翻译 → 阅读过滤标准:
- 保留:AI/ML、编程、创业、认知科学、设计、科技动态
- 删除:政治、体育、纯硬件、交通基建、娱乐八卦
设计哲学:
- 零代码改动:过滤逻辑由Claude执行,可根据上下文灵活判断
- 符合v2.7原则:机械的事交给脚本,智能的事交给AI
- 用户体验优先:减少80%噪音,聚焦真正感兴趣的内容
效果:
- 改进前:抓取30篇 → 其中20篇无关 → 手动跳过
- 改进后:抓取30篇 → 自动过滤 → 只剩10篇精选
v2.9 (2026-01-06) - 完整历史记录修复 📚
核心改进: 1. 恢复 content_log.md 更新:
- 修复自v2.2后停止更新的bug
- 所有抓取的文章自动追加到 content_log.md
- 只增不减,永久保存完整历史
2. 双轨存储架构:
- content_log.md - 完整历史档案(所有文章,永不删除)
- 周刊文件 - 工作队列(待处理文章,会被修改)
- archived/ - 归档存储(已读-不感兴趣)
- ⭐️收藏精选.md - 精选库(值得收藏)
3. 设计哲学:关注点分离:
- content_log.md = "档案馆"(查历史、搜索、统计)
- 周刊 = "工作台"(当前任务、标记处理)
- 归档 = "冷存储"(已读内容)
- 收藏 = "精选库"(反复阅读)
问题回顾:
- 改进前:v2.2迁移到周刊工作流后,content_log.md 停止更新,历史记录缺失
- 改进后:双轨存储,既有灵活的工作队列,又有完整的永久档案
效果:
- ✅ 完整历史可追溯(content_log.md)
- ✅ 工作流更清晰(周刊 = 待办事项)
- ✅ 数据永不丢失(只增不减)
v2.8 (2026-01-06) - 智能翻译 + HN优化 🌐
核心改进: 1. 自动翻译标题(核心功能):
- 抓取完成后,Claude自动翻译所有英文标题
- 格式:
English Title / 中文翻译 - 保持技术术语准确性(Transformer、AGI等保留原文)
- 使用Edit工具批量处理,高效准确
2. HackerNews数量优化:
- 限制从30篇降至8篇(减少73%噪音)
- 只保留最热门话题,提高信噪比
- 聚焦质量而非数量
3. 工作流简化:
- 抓取 → 自动翻译 → 直接阅读
- 无需手动请求翻译
- 一步到位,零摩擦
设计哲学:
- 自动化重复任务 - 让AI做它最擅长的事(翻译)
- 降低认知负担 - 双语标题,中文读者友好
- 精准 > 数量 - 8篇精选好过30篇噪音
- 即时可用 - 抓取即翻译,无需二次处理
效果提升:
- 改进前:抓取30篇HN → 手动请求翻译 → 等待处理
- 改进后:抓取8篇HN → 自动翻译 → 立即阅读
v2.4 (2026-01-05) - Checkbox工作流优化 ✅
核心改进: 1. Obsidian友好的Checkbox格式:
- 所有新抓取的素材自动添加checkbox
- [ ] - 在Obsidian中可以点击切换
[ ]↔[x] - 后续字段自动缩进对齐,视觉清爽
2. 极简标记系统(只需两个操作):
- 点击checkbox
[x]→ 已读/不感兴趣(自动归档) - 添加⭐️ → 收藏(转移到收藏库)
- 消除了4种emoji标记的复杂性
3. 自动化处理脚本:
process_marked_items.py- 一键处理所有标记- [x]无emoji → 按周归档到archived/2026-W02.md- [x] ⭐️→ 转移到⭐️收藏精选.md- [ ]→ 保持原位(待处理)
4. 格式迁移工具:
migrate_to_checkbox_format.py- 将旧格式转换为新格式- 自动备份原文件(.md.backup)
- 向后兼容,无需手动修改
设计哲学:
- 简化是最高形式的复杂 - 用两个维度替代4种emoji
- 消除特殊情况 - checkbox状态 + 可选emoji,统一处理逻辑
- 降低标记成本 - Obsidian点击 vs 手动输入emoji
- 自动化归档 - 减少手动整理,聚焦阅读和创作
效果提升:
- 改进前:手动输入4种emoji(⭐️📒✅❌),需要记忆含义
- 改进后:点击checkbox + 可选⭐️,自动归档和转移
v2.3 (2026-01-03) - LLM增强推荐 🤖✨
核心改进: 1. 智能翻译:
- Claude 自动翻译所有英文标题为中文
- 保持技术术语准确性(如 Transformer、AGI 等保留原文)
- 格式:
英文标题 / 中文翻译
2. 综合评分系统:
- 兴趣匹配度 (0-5⭐):与用户AI兴趣的相关度
- 深度潜力 (0-5⭐):内容的深度和洞察价值
- 写作可行性 (0-5⭐):素材是否充足、角度是否清晰
- 总分 = 兴趣×0.4 + 深度×0.4 + 可行性×0.2
3. 中文推荐理由:
- 为什么值得写、能带来什么价值、时机为什么好
- 提供乔木风格的写作大纲(开头/核心观点/类比/结尾)
- 所有推荐全中文呈现,易读易懂
4. 双文件输出:
recommendations.md- 原始英文推荐(供参考)recommendations_enhanced.md- 增强版中文推荐(主要使用)
设计哲学:
- 充分利用 LLM 能力:翻译、打分、写作建议,而非硬编码规则
- 用户友好:中文推荐,直接可用,无需二次加工
- 保留原始数据:英文版仍保留,供需要时查看
效果提升:
- 改进前:英文推荐 + 简单模板,需要手动翻译理解
- 改进后:中文推荐 + 综合打分 + 写作大纲,立即可写
v2.2 (2026-01-03) - 路径配置化重构 🏗️
核心改进: 1. 输出路径配置化:
- 新增
config/paths.json统一管理所有输出路径 - 所有用户可见文件迁移到
~/乔木新知识库/01.inbox/素材索引/ - 内部状态文件(
state.json)保留在技能目录
2. 代码架构优化:
- 新增
scripts/shared/path_utils.py公共路径工具模块 - 消除所有硬编码路径,实现配置化
- 向后兼容:若配置文件不存在,自动回退到默认路径
3. 文件组织哲学:
- 关注点分离:技术细节 vs 用户输出
- 单一真相源:所有路径配置集中在一处
- 用户友好:输出文件在知识库中,便于管理和访问
修改的文件:
config/paths.json- 新增scripts/shared/path_utils.py- 新增fetch_updates.py- 路径配置化recommend_topics.py- 路径配置化generate_weekly_index.py- 路径配置化generate_starred_index.py- 路径配置化
效果提升:
- 改进前:输出文件分散在技能目录和知识库两个位置
- 改进后:所有输出集中在知识库
01.inbox/素材索引/,统一管理
v2.1 (2026-01-03) - 推荐体验优化 + 自动通知 🔔
核心改进: 1. 推荐引擎增强:
- 标题带有直接可点击的Markdown链接
- 根据来源类型自动添加上下文说明:
- 💬 HN排名信息(如"HN排名#10")
- 📄 HuggingFace论文标注
- 🎙️ Import AI深度分析标识
- 📰 TLDR AI快讯说明
- 无需手动复制URL,点击即可阅读原文
2. 自动运行 + macOS通知:
- 新增
auto_fetch_notify.sh包装脚本 - 每天早9点自动抓取最新内容
- 通过macOS Notification Center推送通知:
- ✅ 有新文章:显示数量
- ℹ️ 无新文章:友好提示
- ❌ 失败时:提醒查看日志
- 日志记录到
data/auto_run.log
3. launchd配置升级:
- 添加环境变量配置(支持conda/virtualenv)
- 独立日志文件(
launchd.log,launchd_error.log) - 手动测试命令:
launchctl start com.user.qiaomu-ai-radar
效果提升:
- 改进前:推荐只有标题和URL,需要手动复制链接
- 改进后:标题可点击 + 来源上下文 + 每日自动通知
v2.0 (2025-12-27) - 智能推荐升级 🎯
重大改进: 1. 用户偏好系统:新增 config/user_preferences.json,支持个性化过滤
- 主要兴趣关键词(AI、LLM、AGI等50+关键词)
- 次要兴趣(工程哲学、产品设计、创业故事)
- 排除主题(DevOps、基础设施等)
2. 智能过滤算法:
- 从112篇文章 → 自动筛选到38篇AI相关(减少66%噪音)
- 兴趣匹配评分:主要兴趣+3分,次要+1分
- 深度评分:Import AI(5分)、HF Papers(4分)、Wait But Why(5分)
3. 主题聚类:
- AI模型与算法
- AI应用与工具
- AI政策与伦理
- AI哲学与认知
- 工程哲学
- 创业与产品
4. 乔木风格推荐模板: 每个推荐包含:
- 开头句式("你有没有想过...")
- 核心观点模板("本质不是X,而是Y")
- 类比句式("就像...也是...")
- 结尾升华("技术的价值不在于...")
效果对比:
- 改进前:推荐所有112篇,包含大量Linux、数据库、网络等无关内容
- 改进后:精准推荐38篇AI方向,每个推荐都有写作角度建议
v1.0 (2025-12-24) - 初始版本
基础功能:内容抓取、URL去重、简单推荐
---
概述
这个skill帮助用户发现每日值得创作的话题。通过监控多个内容源(newsletter、博客、社区),自动采集最新文章并基于用户偏好提供个性化选题建议。
核心功能: 1. 内容监控:自动抓取 Import AI、Hugging Face Papers、Hacker News、James Clear、Wait But Why、Product Hunt 等高质量信息源 2. URL去重:记录已见过的文章,只关注新内容 3. 智能过滤:基于用户兴趣自动过滤无关内容(v2.0新增) 4. 主题聚类:将文章按主题分类,便于发现选题方向(v2.0新增) 5. 乔木风格推荐:每个推荐包含写作角度模板(v2.0新增)
当前信息源:
- 🤖 Import AI (周更) - Anthropic 联创 Jack Clark 的深度时事通讯,AI 政策与研究方向
- 📄 Hugging Face Papers (日更) - 社区投票的热门 AI 论文
- 📰 Hacker News (日更) - 技术社区热门话题,工程哲学与设计思考
- 💡 James Clear 3-2-1 (周更) - 认知方法论与生活智慧
- 📝 Wait But Why (不定期) - 深度长文,科技、人生、社会议题
- 🚀 Product Hunt (日更) - 新产品发布
技能目录:~/.agents/skills/qiaomu-ai-radar/ 输出目录:~/乔木新知识库/20-29 学习/21 素材索引/(v2.2 配置化)
---
使用模式
模式A:抓取更新 + 自动翻译 (v2.11升级)
触发词:
- "检查今天的更新"
- "抓取最新文章"
- "更新内容库"
执行流程: 1. 执行抓取脚本 fetch_updates.py 2. 🚨 强制全量翻译(脚本输出会显示 CLAUDE_ACTION_REQUIRED: TRANSLATE_TITLES):
- 这一步是强制性的,Claude必须立即执行,不可跳过
- Claude需要:
1. 读取周刊文件(脚本输出的路径) 2. 扫描整个文件,找到所有未翻译的英文标题 3. 未翻译的定义:标题行不包含" / "分隔符
- ❌ 未翻译:
**标题**: The Future of AI - ✅ 已翻译:
**标题**: The Future of AI / AI的未来
4. 翻译所有未翻译标题,格式:English Title / 中文翻译 5. 用Edit工具批量写回(每次处理5-10个,避免超时)
- 翻译规范:
- 技术术语保留原文:Transformer, AGI, LLM, API, SDK
- 专有名词保留原文:Claude, OpenAI, GPT
- 用简洁口语化中文,不用书面语
3. ☁️ 强制同步飞书本周索引(翻译完成后执行,不可跳过):
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 sync_feishu_weekly.py "脚本输出的周刊文件路径"- 飞书目录名固定为
乔木AI雷达 - 本周文档首次创建,后续抓取覆盖更新同一份
- 如果缺少飞书授权或 scope,必须如实报告 lark-cli 输出的缺失 scope / auth hint
4. 报告:新发现文章数 + 本次翻译数 + 飞书同步状态 + 翻译完成状态 5. 🚀 自动打开 Obsidian(强制执行,不可跳过):
- 使用 Obsidian URI 格式(重要:必须 URL 编码,不带 .md 扩展名):
open "obsidian://open?vault=%E4%B9%94%E6%9C%A8%E6%96%B0%E7%9F%A5%E8%AF%86%E5%BA%93&file=20-29%20%E5%AD%A6%E4%B9%A0%2F21%20%E7%B4%A0%E6%9D%90%E7%B4%A2%E5%BC%95%2FYYYY-%E7%AC%ACN%E5%91%A8"- 其中
YYYY-第N周替换为当前周刊文件名(需 URL 编码) - 编码规则:
- vault 名称
乔木新知识库→%E4%B9%94%E6%9C%A8%E6%96%B0%E7%9F%A5%E8%AF%86%E5%BA%93 - 路径中的空格 →
%20 - 路径中的
/→%2F - 中文字符需 URL 编码
- 不要加 .md 扩展名
- 用户可以立即在 Obsidian 中阅读和标记
⚠️ 关键:翻译范围是整个周刊中所有未翻译的标题,不仅仅是本次新增的! ⚠️ 关键:翻译完成后必须再次同步飞书,不能只同步未翻译的抓取结果! ⚠️ 关键:翻译完成后必须自动打开 Obsidian,让用户直接阅读!
示例:
用户:检查今天的更新
Claude:
🔍 正在检查各个内容源的更新...
[执行 fetch_updates.py]
✅ 发现 8 篇新文章:
📧 Newsletter (3篇):
- James Clear: 3-2-1: december-24-2025
- Ben's Bites: AI Safety Updates
- Ben's Bites: New Models Released
📰 Hacker News (3篇):
- Show HN: I built a...
- Ask HN: What are you working on?
- The future of AI agents
🚀 Product Hunt (2篇):
- SuperApp - All-in-one productivity
- CodeAssist - AI coding assistant
💾 已保存到素材库:~/.agents/skills/qiaomu-ai-radar/data/content_log.md模式B:推荐选题(v2.2升级 - 智能翻译)
触发词:
- "今天值得写什么"
- "推荐选题"
- "有什么好话题"
执行流程(v2.2 智能翻译 + 综合打分): 1. 基础推荐生成 (Python脚本):
- 加载用户偏好
config/user_preferences.json - 读取最近 3 天的 content_log.md(原始素材)
- 智能过滤:按用户兴趣筛选文章
- 主题聚类:按6大主题分类
- 深度评分:兴趣匹配分 + 深度分
2. LLM增强处理 (Claude自动执行):
- 读取脚本生成的原始推荐
- 翻译标题:所有英文标题翻译成中文
- 综合打分:
- 兴趣匹配度(0-5⭐)
- 深度潜力(0-5⭐)
- 写作可行性(0-5⭐)
- 总分 = 三项加权平均
- 生成中文推荐理由:为什么值得写、如何切入
- 提供写作大纲:开头、核心、类比、结尾的具体建议
3. 输出增强版推荐:
- 保存到
recommendations_enhanced.md(中文版) - 保留原始
recommendations.md(英文版,供参考)
推荐逻辑(v2.0): 1. 兴趣过滤(第一轮筛选)
- 主要兴趣关键词匹配:+3分
- 次要兴趣关键词匹配:+1分
- 排除关键词匹配:直接过滤
2. 深度评分(第二轮排序)
- Import AI:+5分(深度政策分析)
- Hugging Face Papers:+4分(研究前沿)
- Wait But Why:+5分(深度思考)
- HN包含"why/philosophy/design":+2分
- 标题长度>50字符:+1分
3. 主题聚类(第三轮组织)
- AI模型与算法:transformer、training、inference等
- AI应用与工具:agent、coding assistant、workflow等
- AI政策与伦理:safety、alignment、regulation等
- AI哲学与认知:reasoning、consciousness、emergence等
- 工程哲学:design、architecture、senior等
- 创业与产品:startup、bootstrapped、revenue等
快速推荐命令:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 recommend_topics.py配置用户偏好: 编辑 config/user_preferences.json:
{
"user_profile": {
"interests": ["AI", "LLM", "AGI", "AI Safety", ...],
"secondary_interests": ["Engineering Philosophy", "Product Design"],
"excluded_topics": ["DevOps", "Infrastructure"]
},
"content_filters": {
"ai_keywords": ["GPT", "Claude", "Transformer", ...],
"exclude_keywords": ["Kubernetes", "Docker Swarm", ...]
}
}示例:
用户:今天值得写什么?
Claude:
💡 基于最近3天采集的内容,这里有3个值得写的选题:
## 1. AI Safety的现实困境(热点 + 深度)
**为什么推荐**:
- Ben's Bites连续2天提到AI安全话题
- Hacker News有相关讨论(200+评论)
- 当前热度高,但缺少通俗解读
**可以结合的素材**:
- Ben's Bites: "AI Safety Updates" (2025-12-23)
- HN: "The future of AI agents" (2025-12-24)
**切入角度**:
- 不要讲抽象的"对齐问题"
- 从具体案例入手:AI助手的错误决策
- 类比:就像教小孩,规则设计很重要
- 落脚点:普通人如何理解和应对AI的局限性
---
## 2. 从产品设计看认知负荷(实用 + 洞察)
**为什么推荐**:
- Product Hunt昨天有一个"All-in-one"产品
- James Clear最近讲了"用伟大方式对待小事"
- 可以结合成:为什么简单产品更好用
**可以结合的素材**:
- Product Hunt: "SuperApp" (2025-12-23)
- James Clear: 3-2-1 (2025-12-18)
**切入角度**:
- SuperApp想做All-in-one,但用户真的需要吗?
- 引用James Clear:专注做好一件事
- 延伸到生活:少即是多的智慧
---
## 3. Show HN背后的创作动力(认知)
**为什么推荐**:
- HN最近有多个"Show HN"项目
- 可以探讨:为什么有人愿意公开展示作品
- 结合Wait But Why的深度风格
**可以结合的素材**:
- HN: "Show HN: I built a..." (2025-12-24)
**切入角度**:
- 公开展示 vs 完美主义
- 社区反馈的价值
- Done is better than perfect---
模式C:处理已标记素材(v2.4新增)⭐
触发场景:
- 在Obsidian中阅读周刊文件
- 用checkbox和emoji标记素材
- 运行处理脚本自动归档和转移
标记规则:
- [ ] **标题**: ... → 待处理(保持原位)
- [x] **标题**: ... → 已读-不感兴趣(归档到archived/)
- [x] ⭐ **标题**: ... → 已读-收藏(转移到⭐️收藏精选.md)执行命令:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 process_marked_items.py处理逻辑: 1. 扫描所有周刊文件(2026-第N周.md) 2. 识别标记状态:
[x]无emoji → 按周归档到archived/2026-W02.md[x] ⭐️→ 转移到⭐️收藏精选.md[ ]→ 保持原位
3. 更新原文件,移除已处理条目 4. 输出处理统计
示例输出:
🔍 扫描 2 个周刊文件...
📄 处理 2026-第2周.md...
📦 归档 15 条到 2026-W02.md
⭐ 收藏 3 条到 ⭐️收藏精选.md
✅ 已更新原文件
==================================================
✨ 处理完成!
📦 归档: 15 条
⭐ 收藏: 3 条设计哲学:
- 降低标记成本 - 点击checkbox比输入emoji更快
- 自动化归档 - 无需手动整理,专注阅读
- 保持追溯 - 归档按周组织,可随时查看
- 极简操作 - 只需两个维度:checkbox + 可选⭐️
迁移旧格式: 如果有旧格式文件(没有checkbox),运行迁移脚本:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 migrate_to_checkbox_format.py---
Workflow详解
步骤0:理解用户意图
读取用户消息,判断是:
- 模式A(抓取更新):包含"检查"、"抓取"、"更新"等关键词
- 模式B(推荐选题):包含"推荐"、"选题"、"值得写"等关键词
- 模式C(处理标记):包含"归档"、"处理标记"、"清理"等关键词
步骤1:执行对应操作
如果是模式A(抓取更新):
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 fetch_updates.py脚本会: 1. 读取config/sources.json,遍历所有enabled的源 2. 调用对应的parser抓取内容 3. 通过URL hash去重(与data/state.json对比) 4. 将新文章追加到本周周刊(checkbox格式) 5. 更新state.json
抓取完成后自动过滤 + 翻译(v2.10升级):
IMPORTANT: 以下步骤由 Claude 自动执行,无需用户手动请求。
---
过滤+翻译处理(v2.17 强制验证模式)⚠️ (2026-04-14)
核心原则:必须实际执行 Edit 操作,不能只说不做
问题根源:
- Claude 容易"幻觉":说"翻译完成"但实际没调用 Edit 工具
- 用户看到的文件没有任何变化,导致信任崩塌
强制验证机制:
1. Read 文件,扫描未翻译的英文标题(不含" / "的行)
2. 翻译规则:
- 翻译格式:
English Title / 中文翻译 - 技术术语保留原文:Transformer, AGI, LLM, GPT, API, SDK, CLI
- 专有名词保留原文:Claude, OpenAI, Anthropic, Google
- 用简洁口语化中文,不用书面语
3. 🚨 强制执行 Edit(parallel 并行发出所有 Edit 调用):
old: - [ ] **标题**: English Title
new: - [ ] **标题**: English Title / 中文翻译⚠️ 关键检查点:
- 必须看到
<invoke name="Edit">标签 - 必须看到
<output>The file ... has been updated successfully</output> - 如果没有这些输出,说明你只是"想象"了翻译,实际没做
4. 验证并报告:
- 统计成功的 Edit 调用数量(从工具返回结果中计数)
- 报告格式:
✅ 翻译完成!实际执行了 X 次 Edit 操作 - 禁止说"翻译完成"而没有实际 Edit 调用
5. ☁️ 强制同步飞书最终版:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 sync_feishu_weekly.py "周刊文件路径"- 这一步必须在翻译 Edit 成功后执行
- 目的是用翻译后的 Markdown 覆盖飞书本周文档
- 如果 lark-cli 报缺 scope / 未授权,报告原始缺失项,不要声称飞书同步成功
如果是模式B(推荐选题):
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 recommend_topics.py脚本会生成初步推荐到 output_dir/recommendations.md。
步骤2:LLM增强推荐(v2.2新增 - Claude自动执行)
IMPORTANT: 这一步由 Claude 自动执行,无需用户干预。
执行条件:
- 仅在"模式B(推荐选题)"时执行
recommend_topics.py成功生成推荐后
执行流程:
1. 读取原始推荐:
读取 output_dir/recommendations.md2. 翻译所有英文标题:
- 使用 Claude 的语言理解能力
- 保持技术术语准确性(如 Transformer 保留原文)
- 格式:
英文标题 / 中文翻译
3. 综合打分(每篇文章):
- 兴趣匹配度 (0-5⭐):与用户AI兴趣的相关度
- 深度潜力 (0-5⭐):内容的深度和洞察价值
- 写作可行性 (0-5⭐):素材是否充足、角度是否清晰
- 总分 = (兴趣×0.4 + 深度×0.4 + 可行性×0.2)
4. 生成中文推荐理由:
- 为什么这个话题值得写
- 能给读者带来什么价值
- 现在写的时机为什么好
5. 提供写作大纲(乔木风格):
- 开头:具体的问题或场景(不要抽象)
- 核心观点:本质是什么,不是什么
- 类比:用生活化的例子解释技术
- 结尾:技术的价值在哪里
6. 输出增强版推荐:
写入 output_dir/recommendations_enhanced.md输出格式示例:
# 📊 今日选题推荐(智能翻译版)
> 🤖 基于AI智能过滤 + Claude翻译打分
> 📅 生成时间:2026-01-03
> 📈 综合评分 = 兴趣×0.4 + 深度×0.4 + 可行性×0.2
---
## 1. IQuest-Coder:开源代码模型的突破 ⭐⭐⭐⭐
**原标题**: IQuest-Coder: A new open-source code model beats Claude Sonnet 4.5 and GPT 5.1
**综合评分**: 4.2/5 ⭐⭐⭐⭐
- 兴趣匹配: ⭐⭐⭐⭐⭐ (AI模型)
- 深度潜力: ⭐⭐⭐⭐ (有技术报告)
- 写作可行性: ⭐⭐⭐⭐ (素材充足)
**为什么值得写**:
开源AI代码模型声称超越Claude和GPT,这是一个重大突破。读者关心的不是benchmark数字,而是"开源真的能超越闭源了吗?"这是一个绝佳的切入点。
**写作大纲**:
- **开头**: "你在用GitHub Copilot吗?如果我告诉你,有个开源模型比它更强,你会相信吗?"
- **核心观点**: 代码模型的竞争不是参数多少,而是理解程序员意图的能力
- **类比**: 就像翻译不是单词对单词,编程助手也不是代码对代码
- **结尾**: 开源的意义不在于免费,而在于可控和透明
**参考素材**:
- [技术报告PDF](https://github.com/IQuestLab/IQuest-Coder-V1/blob/main/papers/IQuest_Coder_Technical_Report.pdf)
- HN讨论区:查看社区反馈和质疑
---步骤3:生成报告
模式A:
- 统计新文章数量
- 按来源分类列出标题
- 告知保存位置
模式B:
- 读取增强版推荐
recommendations_enhanced.md - 向用户展示:
- 翻译后的中文标题
- 综合评分和星级
- 推荐理由和写作大纲
- 参考素材链接
- 提供下一步行动建议(选择某个话题开始写作)
---
自动化:每日自动运行 + 通知(v2.1)
macOS使用launchd + macOS Notification Center,每天早9点自动抓取并推送通知。
✅ 已自动配置(v2.1)
launchd任务已配置完成,无需手动设置。系统会: 1. 每天早上9点自动运行抓取脚本 2. 通过macOS通知中心推送结果:
- 📚 有新文章 → 显示数量 + 玻璃音效
- 💤 无新文章 → 友好提示
- ⚠️ 出现错误 → 提醒查看日志
手动管理命令
# 查看任务状态
launchctl list | grep daily-topic
# 手动触发(测试通知)
launchctl start com.user.qiaomu-ai-radar
# 重新加载配置(修改时间后)
launchctl unload ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist
launchctl load ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist
# 禁用自动运行
launchctl unload ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist修改运行时间
编辑 ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist,修改Hour和Minute:
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer> <!-- 改为你想要的小时(24小时制) -->
<key>Minute</key>
<integer>0</integer> <!-- 改为你想要的分钟 -->
</dict>修改后重新加载配置:
launchctl unload ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist
launchctl load ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist查看运行日志
# 自动运行日志(包含抓取结果)
tail -50 ~/.agents/skills/qiaomu-ai-radar/data/auto_run.log
# launchd系统日志
tail -20 ~/.agents/skills/qiaomu-ai-radar/data/launchd.log
# 错误日志
tail -20 ~/.agents/skills/qiaomu-ai-radar/data/launchd_error.log---
文件结构
~/.agents/skills/qiaomu-ai-radar/
├── VERSION # 2.2
├── SKILL.md # 本文件(使用手册)
├── CLAUDE.md # 架构文档(设计哲学)
├── README.md # 用户文档
├── metadata.json # Skill元数据
├── config/
│ ├── sources.json # 内容源配置
│ ├── user_preferences.json # 用户偏好配置(v2.0)
│ ├── paths.json # ✨ 输出路径配置(v2.2)
│ └── filters.json # 过滤规则
├── scripts/
│ ├── fetch_updates.py # 核心抓取脚本
│ ├── sync_feishu_weekly.py # 飞书本周索引同步入口
│ ├── recommend_topics.py # 智能推荐引擎(v2.0)
│ ├── auto_fetch_notify.sh # 自动运行包装脚本(v2.1)
│ ├── generate_weekly_index.py # 周刊索引生成器
│ ├── generate_starred_index.py # 标星文章索引生成器
│ ├── list_articles.py # 文章列表工具
│ ├── sync_status.py # 状态同步工具
│ ├── parsers/ # 解析器库
│ │ ├── importai.py # [RSS] Import AI newsletter
│ │ ├── huggingface.py # [HTML] Hugging Face Papers
│ │ ├── hackernews.py # [聚合器] Hacker News
│ │ ├── tldrai.py # [RSS] TLDR AI
│ │ ├── bensbites.py # [RSS] Ben's Bites
│ │ ├── jamesclear.py # [归档] James Clear 3-2-1
│ │ ├── waitbutwhy.py # [博客] Wait But Why
│ │ └── producthunt.py # [聚合器] Product Hunt
│ └── shared/
│ ├── web_utils.py # 共享工具(curl wrapper)
│ ├── feishu_sync.py # 飞书目录/文档创建与覆盖同步
│ └── path_utils.py # ✨ 路径配置加载器(v2.2)
└── data/
├── state.json # URL去重状态(保留在技能目录)
├── feishu_sync.json # 飞书目录与每周文档 token 映射
├── auto_run.log # 自动运行日志(v2.1)
├── launchd.log # launchd标准输出(v2.1)
└── launchd_error.log # launchd错误输出(v2.1)
用户输出文件(v2.2 迁移到知识库):
~/乔木新知识库/20-29 学习/21 素材索引/
├── content_log.md # 历史素材库(按日期组织)
├── recommendations.md # 最新推荐(v2.0生成)
├── 2025-第1周.md # 周刊索引
├── 2025-第2周.md
└── ⭐️收藏精选.md # 收藏索引(跨周累积)---
翻译周刊索引(v2.0最佳实践)
场景:generate_weekly_index.py 生成的周刊索引包含大量英文标题,需要添加中文翻译。
✅ 推荐方式:直接用 Claude 的 LLM 能力批量翻译
理由:
- 无需外部 API 调用
- 准确理解技术术语上下文
- 保持术语一致性(如 Transformer 保留原文,Vision Language Model 翻译为"视觉语言模型")
- 处理速度快(批量 Edit 操作)
操作流程: 1. 用户请求:"给周刊索引添加中文翻译" 2. Claude 读取文件(如 素材索引/2025-第52周.md) 3. 分批处理(每次30-50篇),用 Edit 工具直接修改 4. 格式:### English Title / 中文翻译
示例:
### The Illustrated Transformer / 图解Transformer
### Nvidia buying AI chip startup Groq for about $20B in cash / 英伟达以约200亿美元现金收购AI芯片初创公司Groq❌ 不推荐:
- 创建翻译脚本调用外部 API(过度工程)
- 批量调用翻译服务(成本高、质量差)
设计哲学:能用 LLM 直接解决的事,不要写脚本。简单、高效、零依赖。
---
配置说明
sources.json字段
{
"id": "唯一标识",
"name": "显示名称",
"url": "抓取URL",
"type": "类型(newsletter_archive/blog/aggregator)",
"parser": "解析器名称",
"enabled": true,
"check_interval_hours": 24,
"limit": 10,
"last_checked": "ISO时间戳"
}添加新的内容源
1. 在config/sources.json添加配置 2. 在scripts/parsers/创建对应的解析器.py 3. 解析器返回格式:
[
{
"url": "文章URL",
"title": "标题",
"summary": "摘要(可选)"
}
]4. 在fetch_updates.py的PARSERS字典中注册
---
故障排查
| 问题 | 解决方案 |
|---|---|
| 抓取失败 | 检查网络连接,curl能否访问目标网站 |
| 解析失败 | 网站结构可能变化,需要更新解析器的正则表达式 |
| 没有新文章 | 可能目标网站没有更新,或已经抓取过 |
| cron不执行 | 检查launchctl状态,查看error日志 |
| state.json过大 | 定期清理旧记录(保留最近90天即可) |
---
使用示例
示例1:每日检查
用户:检查今天的更新
Claude:
[执行fetch_updates.py]
✅ 发现 5 篇新文章,已保存到素材库。
主要内容:
- James Clear发布了新一期3-2-1
- HN有个关于AI代理的热门讨论
- Product Hunt昨天上线了一个新的AI工具示例2:请求选题
用户:今天值得写什么?
Claude:
[读取content_log.md最近内容]
💡 推荐3个选题:
1. **AI代理的实用困境**
- HN热门讨论 + James Clear的快速恢复理论
- 切入角度:AI不完美,但恢复能力很重要
2. **Product Hunt的产品哲学**
- 分析昨天的top产品设计
- 切入角度:简单 vs 功能丰富
3. **深度工作的现代挑战**
- 结合Wait But Why的长文
- 切入角度:信息过载时代如何保持专注---
版本历史
- v1.0 (2025-12-24) - 初始版本
- 支持5个内容源
- URL去重
- 选题推荐逻辑
- macOS launchd定时任务
MIT License
Copyright (c) 2026 joeseesun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "qiaomu-ai-radar",
"version": "1.0",
"description": "每日选题助手 - 监控多个内容源,自动采集最新文章并推荐值得创作的话题。支持James Clear、Ben's Bites、Wait But Why、Hacker News、Product Hunt等。",
"author": "joe",
"triggers": [
"检查今天的更新",
"抓取最新文章",
"更新内容库",
"今天值得写什么",
"推荐选题",
"有什么好话题"
],
"features": [
"自动监控5+内容源",
"URL去重,只关注新内容",
"macOS定时任务支持(每天9am)",
"智能选题推荐,结合乔木写作风格",
"多来源热度分析"
],
"created": "2025-12-24"
}qiaomu-ai-radar
每天早上,把 AI 论文、技术博客、Newsletter、Hacker News、GitHub Trending 里的新东西收进 Obsidian,翻成中文标题,同步到飞书,再给你推荐值得写的选题。
A daily AI/tech radar skill that collects sources, translates weekly indexes, syncs to Feishu, opens Obsidian, and recommends writing topics.
   
[中文](#中文) | [English](#english)
---
<a name="中文"></a>
中文
你每天打开一堆信息源,真正值得写的内容只有几条。
乔木 AI 雷达做这件小而烦的事:抓取、去重、写入周刊、翻译标题、同步飞书、打开 Obsidian。你只需要读和判断。
你会得到什么
## 2026-05-28
### Hugging Face Papers
- [ ] **标题**: MobileGym: A Verifiable and Highly Parallel Simulation Platform for Mobile GUI Agent Research / MobileGym:面向移动 GUI Agent 研究的可验证高并行仿真平台
- **链接**: https://huggingface.co/papers/2605.26114
- **摘要**: Hugging Face 社区热门论文核心能力:
- 自动抓取 AI/技术/创作相关内容源。
- URL 去重,避免每天看到同一篇。
- 生成 Obsidian 友好的周刊 Markdown。
- 将英文标题补成
English / 中文。 - 同步本周索引到飞书目录
乔木AI雷达。 - 用 checkbox 处理已读、收藏、归档。
- 基于偏好推荐值得写的选题。
一行安装
npx skills add joeseesun/qiaomu-ai-radar安装后确认目录存在:
ls ~/.agents/skills/qiaomu-ai-radar前置条件
- [ ] Python 3 可用:
python3 --version - [ ] macOS + Obsidian 已安装,并有名为
乔木新知识库的 vault。 - [ ] 可访问目标 RSS/网页源,脚本使用
curl抓取。 - [ ] 如果要同步飞书,安装并登录
lark-cli,然后在config/paths.json里确认feishu.cli路径。 - [ ] 如果要自动定时运行,允许 macOS
launchd加载用户任务。
使用方式
你可以直接对 Codex/Claude/Cursor 说:
运行乔木AI雷达
检查今天的更新
今天值得写什么?
处理已标记素材也可以手动运行:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 run_radar.py --mode fetch推荐选题:
python3 run_radar.py --mode fetch-and-recommend处理已读和收藏:
python3 run_radar.py --mode process-marked配置
输出目录在 config/paths.json:
{
"output_base_dir": "~/乔木新知识库/20-29 学习/21 素材索引"
}内容源在 config/sources.json。你可以启用/禁用来源,或调整每个来源的抓取数量。
用户偏好在 config/user_preferences.json 和 config/source_preferences.json。推荐选题会优先考虑 AI agents、MCP/tool use、推理、模型能力、多模态、编程手艺、产品设计和认知主题。
飞书同步
飞书同步默认开启。配置在 config/paths.json:
{
"feishu": {
"enabled": true,
"cli": "/opt/homebrew/bin/lark-cli",
"identity": "user",
"folder_name": "乔木AI雷达"
}
}手动测试:
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 sync_feishu_weekly.py --dry-run定时运行
cp ~/.agents/skills/qiaomu-ai-radar/com.user.qiaomu-ai-radar.plist ~/Library/LaunchAgents/
sed -i '' "s/YOUR_USERNAME/$USER/g" ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist
launchctl load ~/Library/LaunchAgents/com.user.qiaomu-ai-radar.plist
launchctl list | grep qiaomu-ai-radar风险与限制
- 这个 skill 会写入你的知识库 Markdown 文件。
data/state.json、data/articles.db、日志和飞书同步状态是运行时数据,不应该提交到公开仓库。- RSS 和网页结构会变,某些解析器偶尔会失效。
- 飞书同步依赖本地
lark-cli授权和 scope。 - Obsidian 打开依赖 macOS
open obsidian://...。
Troubleshooting
| 问题 | 处理 |
|---|---|
lark-cli not found | 修改 config/paths.json 里的 feishu.cli,或安装并登录 lark-cli。 |
| 没有新文章 | 可能源没有更新,也可能 URL 已在 data/state.json 中被去重。 |
| 周刊文件没打开 | 确认 Obsidian vault 名称是 乔木新知识库,并且 macOS 能打开 obsidian:// URI。 |
| RSS 抓取失败 | 直接运行 python3 scripts/fetch_updates.py 看具体来源报错;通常是网络或网站结构变化。 |
| 标题没翻译 | 运行 python3 scripts/verify_weekly.py --json 检查残留,再跑 python3 scripts/run_radar.py --mode fetch。 |
---
<a name="english"></a>
English
Qiaomu AI Radar is a daily topic radar skill for AI writers and technical creators.
It collects fresh AI/tech items, deduplicates URLs, writes an Obsidian-friendly weekly Markdown index, translates English titles into Chinese, syncs the weekly index to Feishu, and recommends writing topics.
Install
npx skills add joeseesun/qiaomu-ai-radarVerify:
ls ~/.agents/skills/qiaomu-ai-radarRequirements
- [ ] Python 3:
python3 --version - [ ] macOS + Obsidian with a vault named
乔木新知识库 - [ ] Network access to RSS/web sources
- [ ] Optional:
lark-clifor Feishu sync - [ ] Optional:
launchdfor daily scheduled runs
Natural Language Triggers
Run Qiaomu AI Radar
Check today's updates
What should I write about today?
Process marked articlesCLI Usage
cd ~/.agents/skills/qiaomu-ai-radar/scripts
python3 run_radar.py --mode fetch
python3 run_radar.py --mode fetch-and-recommend
python3 run_radar.py --mode process-markedRuntime Data
Runtime files live under data/ and are intentionally ignored by git:
state.jsonarticles.dbfeishu_sync.json- logs
License
MIT
#!/bin/bash
# ============================================================
# 每日自动抓取 + macOS通知 + Obsidian集成
# v2.8 - 自动写入当日日记
# ============================================================
SKILL_DIR="$HOME/.agents/skills/qiaomu-ai-radar"
LOG_FILE="$SKILL_DIR/data/auto_run.log"
OUTPUT_DIR="$HOME/乔木新知识库/20-29 学习/21 素材索引"
DIARY_DIR="$HOME/乔木新知识库/30-39 成长/35 日记"
export PATH="$HOME/.local/node/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
# 记录开始时间
echo "========================================" >> "$LOG_FILE"
echo "自动运行开始: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
# 切换到scripts目录
cd "$SKILL_DIR/scripts" || exit 1
# 运行统一入口:抓取、翻译、飞书同步、验证、打开 Obsidian
python3 run_radar.py --mode fetch >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
# 计算当前周数,构建周刊文件路径
YEAR=$(date '+%Y')
WEEK=$(date '+%V' | sed 's/^0//')
WEEKLY_FILE="$OUTPUT_DIR/${YEAR}-第${WEEK}周.md"
# 检查是否成功
if [ $EXIT_CODE -eq 0 ]; then
# 统计新文章数量
NEW_COUNT=$(tail -20 "$LOG_FILE" | grep "总计发现" | tail -1 | grep -oE "[0-9]+ 篇" | grep -oE "[0-9]+")
if [ -z "$NEW_COUNT" ]; then
NEW_COUNT=0
fi
if [ "$NEW_COUNT" -gt 0 ]; then
# 快速通知(通知中心)
osascript -e "display notification \"发现 $NEW_COUNT 篇新文章,已同步到素材库\" with title \"📚 每日选题助手\" sound name \"Glass\""
# 可交互对话框(带打开按钮)
if [ -n "$WEEKLY_FILE" ] && [ -f "$WEEKLY_FILE" ]; then
# URL编码文件路径
ENCODED_PATH=$(python3 - "$WEEKLY_FILE" << 'PYEOF'
import sys
from pathlib import Path
from urllib.parse import quote
path = Path(sys.argv[1]).with_suffix("")
vault = Path.home() / "乔木新知识库"
try:
rel = path.relative_to(vault)
except ValueError:
rel = path
print(f"obsidian://open?vault={quote(vault.name, safe='')}&file={quote(str(rel), safe='')}")
PYEOF
)
osascript <<EOF
tell application "System Events"
activate
set userChoice to button returned of (display dialog "发现 $NEW_COUNT 篇新文章!
📊 已更新本周索引
📂 $(basename "$WEEKLY_FILE")" buttons {"稍后查看", "打开 Obsidian"} default button "打开 Obsidian" with title "📚 每日选题助手" with icon note)
if userChoice is "打开 Obsidian" then
do shell script "open '$ENCODED_PATH'"
end if
end tell
EOF
fi
echo "✅ 成功: 发现 $NEW_COUNT 篇新文章" >> "$LOG_FILE"
else
# 无新文章通知(对话框 + 通知中心)
osascript -e "display notification \"今日暂无新内容\" with title \"📚 每日选题助手\""
osascript -e "display dialog \"今日暂无新内容\\n\\n所有信息源均已检查\" with title \"📚 每日选题助手\" buttons {\"知道了\"} default button \"知道了\" giving up after 10"
echo "ℹ️ 今日无新文章" >> "$LOG_FILE"
fi
# ── 写入当日日记 ──────────────────────────────────────────
TODAY=$(date '+%Y-%m-%d')
WEEKDAY=$(LC_TIME=zh_CN.UTF-8 date '+%A' 2>/dev/null || date '+%A')
DIARY_FILE="$DIARY_DIR/${TODAY}.md"
WEEK=$(date '+%V' | sed 's/^0//')
YEAR=$(date '+%Y')
WEEKLY_LINK="[[../../20-29 学习/21 素材索引/${YEAR}-第${WEEK}周]]"
SECTION="## 今日阅读素材
> 由 Topic Selector 自动生成,点击查看 → ${WEEKLY_LINK}
"
if [ ! -f "$DIARY_FILE" ]; then
# 日记不存在:用模板创建
cat > "$DIARY_FILE" << EOF
# ${TODAY} ${WEEKDAY}
${SECTION}
---
## 随手记
---
## 晚间复盘
EOF
echo "📔 创建当日日记: $DIARY_FILE" >> "$LOG_FILE"
else
# 日记已存在:检查是否已有「今日阅读素材」节
if ! grep -q "今日阅读素材" "$DIARY_FILE"; then
# 没有这一节,用 Python 插到标题行之后
python3 - "$DIARY_FILE" "$WEEKLY_LINK" << 'PYEOF'
import sys
diary_path = sys.argv[1]
weekly_link = sys.argv[2]
with open(diary_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
section = f"\n## 今日阅读素材\n\n> 由 Topic Selector 自动生成,点击查看 → {weekly_link}\n\n"
# 在第一行(标题)之后插入
if lines:
lines.insert(1, section)
else:
lines = [section]
with open(diary_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
print("done")
PYEOF
echo "📔 已在日记中插入今日阅读素材" >> "$LOG_FILE"
else
echo "📔 日记已有今日阅读素材,跳过" >> "$LOG_FILE"
fi
fi
# ─────────────────────────────────────────────────────────
else
# 发送失败通知
osascript -e "display notification \"抓取过程出现错误,请查看日志\" with title \"⚠️ 每日选题助手\" sound name \"Basso\""
echo "❌ 失败: 退出码 $EXIT_CODE" >> "$LOG_FILE"
fi
echo "结束时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
#!/usr/bin/env python3
"""
Translate untranslated weekly index titles.
The weekly file is the source of truth for reading. This script only touches
title lines in the current checkbox format:
- [ ] **标题**: English Title
Already bilingual lines containing " / " are left unchanged.
"""
import argparse
import json
import os
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from urllib import request, error
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR / "shared"))
from path_utils import get_weekly_index_path # noqa: E402
TITLE_RE = re.compile(r"^(?P<prefix>\s*- \[[ xX]\] \*\*标题\*\*: )(?P<title>.+?)\s*$")
CJK_RE = re.compile(r"[\u3400-\u9fff]")
REPO_SLUG_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
URL_RE = re.compile(r"^https?://", re.IGNORECASE)
TECH_TERMS = {
"AGI",
"AI",
"API",
"Claude",
"DeepSeek",
"GPT",
"GPU",
"LLM",
"MCP",
"OpenAI",
"SDK",
"Transformer",
}
def current_weekly_file() -> Path:
now = datetime.now()
return get_weekly_index_path(now.year, now.isocalendar()[1])
def is_untranslated_title(title: str) -> bool:
if " / " in title or CJK_RE.search(title):
return False
if REPO_SLUG_RE.match(title) or URL_RE.match(title):
return False
return True
def load_titles(content: str) -> List[Dict[str, object]]:
titles = []
for index, line in enumerate(content.splitlines()):
match = TITLE_RE.match(line)
if not match:
continue
title = match.group("title").strip()
titles.append(
{
"line": index,
"prefix": match.group("prefix"),
"title": title,
"untranslated": is_untranslated_title(title),
}
)
return titles
def translate_with_api(title: str, provider: str, timeout: int = 45) -> Optional[str]:
if provider == "none":
return None
if provider == "deepseek":
api_key = os.environ.get("DEEPSEEK_API_KEY")
base_url = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
model = os.environ.get("QIAOMU_TRANSLATE_MODEL", "deepseek-chat")
url = f"{base_url.rstrip('/')}/chat/completions"
else:
api_key = os.environ.get("OPENAI_API_KEY")
base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
model = os.environ.get("QIAOMU_TRANSLATE_MODEL", "gpt-5-mini")
url = f"{base_url.rstrip('/')}/chat/completions"
if not api_key:
return None
system = (
"Translate article titles into concise natural Chinese. Preserve these "
f"technical/proper terms when appropriate: {', '.join(sorted(TECH_TERMS))}. "
"Return only the Chinese title, no quotes, no explanation."
)
payload = {
"model": model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": title},
],
"temperature": 0.2,
}
data = json.dumps(payload).encode("utf-8")
req = request.Request(
url,
data=data,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
except (error.HTTPError, error.URLError, TimeoutError, json.JSONDecodeError) as exc:
raise RuntimeError(f"{provider} translation failed for {title!r}: {exc}") from exc
choices = body.get("choices") or []
if not choices:
raise RuntimeError(f"{provider} translation returned no choices for {title!r}")
text = choices[0].get("message", {}).get("content", "").strip()
return re.sub(r"^['\"]|['\"]$", "", text).strip() or None
def choose_provider(provider: str) -> str:
if provider != "auto":
return provider
if os.environ.get("DEEPSEEK_API_KEY"):
return "deepseek"
if os.environ.get("OPENAI_API_KEY"):
return "openai"
return "none"
def translate_weekly(
weekly_file: Path,
dry_run: bool = False,
provider: str = "auto",
limit: Optional[int] = None,
) -> Dict[str, object]:
weekly_file = weekly_file.expanduser()
result: Dict[str, object] = {
"ok": False,
"weekly_file": str(weekly_file),
"provider": choose_provider(provider),
"dry_run": dry_run,
"title_count": 0,
"untranslated_before": 0,
"translated_count": 0,
"untranslated_after": 0,
"errors": [],
}
if not weekly_file.exists():
result["errors"] = [f"weekly file not found: {weekly_file}"]
return result
content = weekly_file.read_text(encoding="utf-8")
lines = content.splitlines()
titles = load_titles(content)
untranslated = [item for item in titles if item["untranslated"]]
if limit is not None:
untranslated = untranslated[:limit]
result["title_count"] = len(titles)
result["untranslated_before"] = len([item for item in titles if item["untranslated"]])
provider_name = result["provider"]
for item in untranslated:
title = str(item["title"])
if provider_name == "none":
continue
try:
translated = translate_with_api(title, str(provider_name))
except RuntimeError as exc:
result["errors"].append(str(exc))
continue
if not translated:
continue
new_title = f"{title} / {translated}"
lines[int(item["line"])] = f"{item['prefix']}{new_title}"
result["translated_count"] = int(result["translated_count"]) + 1
time.sleep(0.2)
new_content = "\n".join(lines)
if content.endswith("\n"):
new_content += "\n"
after_titles = load_titles(new_content)
result["untranslated_after"] = len([item for item in after_titles if item["untranslated"]])
if not dry_run and new_content != content:
weekly_file.write_text(new_content, encoding="utf-8")
result["ok"] = not result["errors"]
if provider_name == "none" and result["untranslated_before"]:
result["ok"] = False
result["errors"].append(
"No translation provider configured. Set DEEPSEEK_API_KEY or OPENAI_API_KEY."
)
return result
def main() -> int:
parser = argparse.ArgumentParser(description="翻译乔木 AI 雷达本周周刊标题")
parser.add_argument("weekly_file", nargs="?", help="周刊 Markdown 文件路径;不传则使用本周文件")
parser.add_argument("--dry-run", action="store_true", help="只检查/调用翻译,不写回文件")
parser.add_argument("--json", action="store_true", help="输出 JSON")
parser.add_argument(
"--provider",
choices=["auto", "deepseek", "openai", "none"],
default="auto",
help="翻译提供方,默认自动按环境变量选择",
)
parser.add_argument("--limit", type=int, help="最多处理多少个未翻译标题")
args = parser.parse_args()
weekly_file = Path(args.weekly_file).expanduser() if args.weekly_file else current_weekly_file()
result = translate_weekly(
weekly_file=weekly_file,
dry_run=args.dry_run,
provider=args.provider,
limit=args.limit,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"周刊: {result['weekly_file']}")
print(f"翻译提供方: {result['provider']}")
print(f"标题数: {result['title_count']}")
print(f"翻译前未翻译: {result['untranslated_before']}")
print(f"本次实际翻译: {result['translated_count']}")
print(f"翻译后未翻译: {result['untranslated_after']}")
for message in result["errors"]:
print(f"ERROR: {message}", file=sys.stderr)
return 0 if result["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""
生成收藏精选索引(跨周累积)
"""
import json
import sys
from pathlib import Path
from datetime import datetime, timezone
from collections import defaultdict
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR / "shared"))
from path_utils import get_state_file_path, get_starred_index_path
STATE_FILE = get_state_file_path()
def load_state():
"""加载state.json"""
with open(STATE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def categorize_article(article):
"""根据文章内容自动分类"""
title = article.get("title", "").lower()
url = article.get("url", "").lower()
source = article.get("source", "")
# AI技术类
if any(kw in title for kw in ["llm", "transformer", "model", "audio", "sam", "gpt", "claude", "gemini"]):
return "🤖 AI技术前沿"
# 创业商业类
if any(kw in title for kw in ["builder", "startup", "business", "revenue", "bootstrapped"]):
return "💼 AI创业与商业"
# 工程哲学类
if any(kw in title for kw in ["senior", "job", "machine", "philosophy", "don't"]):
return "💡 工程哲学与职业思考"
# 设计工具类
if any(kw in title for kw in ["design", "manus", "tool"]):
return "🎨 AI产品与设计"
# 历史经典类
if any(kw in title for kw in ["1990", "photoshop", "source code", "history"]):
return "🏛️ 技术历史与经典"
# 实用工具类
if any(kw in title for kw in ["show hn", "html", "png", "gym", "stronk"]):
return "🛠️ 实用工具"
# 默认分类
return "📚 其他精选"
def generate_starred_index():
"""生成收藏精选索引"""
state = load_state()
# 筛选所有收藏文章
starred_articles = [a for a in state["seen_articles"] if a.get("status") == "starred"]
if not starred_articles:
print(" ⚠️ 没有收藏文章")
return
# 按分类分组
by_category = defaultdict(list)
for article in starred_articles:
category = categorize_article(article)
by_category[category].append(article)
# 生成markdown
content = []
content.append("# ⭐️ 收藏精选\n")
content.append("> 📚 不限时间的永久收藏")
content.append("> 🎯 值得深读、反复品味的文章")
content.append(f"> 🔄 最后更新: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n")
content.append("---\n")
# 分类优先级
category_order = [
"🤖 AI技术前沿",
"💼 AI创业与商业",
"🎨 AI产品与设计",
"💡 工程哲学与职业思考",
"🏛️ 技术历史与经典",
"🛠️ 实用工具",
"📚 其他精选"
]
# 按分类输出
for category in category_order:
if category not in by_category:
continue
articles = by_category[category]
content.append(f"\n## {category}\n")
for article in articles:
title = article.get("title", "Untitled")
url = article.get("url", "")
source = article.get("source", "Unknown")
discovered = article.get("discovered_at", "")
# 格式化收藏时间
if discovered:
try:
dt = datetime.fromisoformat(discovered.replace('+00:00', ''))
time_str = dt.strftime("%Y-%m-%d")
except:
time_str = discovered[:10]
else:
time_str = "未知"
content.append(f"### {title}")
content.append(f"- **URL**: {url}")
content.append(f"- **来源**: {source}")
content.append(f"- **收藏时间**: {time_str}")
content.append("")
content.append("\n---\n")
content.append(f"\n## 📊 统计\n")
content.append(f"- **总收藏**: {len(starred_articles)} 篇")
content.append(f"- **分类分布**:")
for category in category_order:
if category in by_category:
count = len(by_category[category])
content.append(f" - {category}: {count}篇")
content.append(f"\n---\n")
content.append(f"\n**生成时间**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
content.append("**更新频率**: 每次运行采集脚本时自动更新")
# 写入文件
index_file = get_starred_index_path()
index_file.parent.mkdir(parents=True, exist_ok=True)
with open(index_file, "w", encoding="utf-8") as f:
f.write("\n".join(content))
print(f"✅ 生成收藏索引: {index_file}")
print(f" 共 {len(starred_articles)} 篇收藏")
# 分类统计
if by_category:
print(f"\n 📊 分类分布:")
for category in category_order:
if category in by_category:
count = len(by_category[category])
print(f" {category}: {count}篇")
if __name__ == "__main__":
try:
generate_starred_index()
except Exception as e:
print(f"\n❌ 生成失败: {e}")
import traceback
traceback.print_exc()
#!/usr/bin/env python3
"""
生成每周素材索引(带emoji状态标记)
"""
import json
import re
import sys
from pathlib import Path
from datetime import datetime, timezone
from collections import defaultdict
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR / "shared"))
from path_utils import get_state_file_path, get_weekly_index_path
STATE_FILE = get_state_file_path()
# 状态到emoji的映射
EMOJI_MAP = {
"read": "✅",
"starred": "⭐️",
"to_write": "📒",
"skipped": "❌",
"urgent": "🔥"
}
def load_state():
"""加载state.json"""
with open(STATE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def get_week_number(date_str):
"""获取ISO周数"""
dt = datetime.fromisoformat(date_str.replace('+00:00', ''))
return dt.isocalendar()[1]
def get_article_emoji(article):
"""根据文章状态返回emoji"""
status = article.get("status", "unread")
if status in EMOJI_MAP:
return f" {EMOJI_MAP[status]}"
return ""
def generate_index_for_week(week_num, year=2025):
"""生成指定周的索引(Inbox模式:只显示未处理的文章)"""
state = load_state()
# 筛选本周的文章
week_articles = []
for article in state["seen_articles"]:
discovered = article.get("discovered_at", "")
if not discovered:
continue
article_week = get_week_number(discovered)
if article_week == week_num:
# Inbox模式:过滤已处理的文章
status = article.get("status", "unread")
# 跳过已读、已跳过、已收藏的文章
if status in ["read", "skipped", "starred"]:
continue
week_articles.append(article)
if not week_articles:
print(f" ⚠️ 第{week_num}周没有文章")
return
# 按来源分组
by_source = defaultdict(list)
for article in week_articles:
source = article.get("source", "Unknown")
by_source[source].append(article)
# 生成markdown
content = []
content.append(f"# {year}年第{week_num}周素材汇总 📥\n")
content.append("> 💡 **Inbox模式**:本索引只显示未处理的文章")
content.append("> - ✅已读、❌跳过、⭐️收藏的文章已自动移除")
content.append("> - 📒待写作的文章仍在此处")
content.append("> - 周末目标:清空此索引 = 全部处理完\n")
content.append(f"> 📅 采集周次: 第 {week_num} 周")
content.append(f"> 📊 待处理: {len(week_articles)} 篇")
content.append(f"> 🔗 来源: {', '.join(sorted(by_source.keys()))}")
content.append(f"> 🔄 最后更新: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n")
content.append("---\n")
# 来源优先级
source_priority = {
"Ben's Bites": 1,
"TLDR AI": 2,
"Import AI": 3,
"Hugging Face Papers": 4,
"Hacker News": 5,
"Product Hunt": 6,
"Wait But Why": 7,
"James Clear 3-2-1": 8
}
# 按优先级排序
sorted_sources = sorted(by_source.keys(), key=lambda x: source_priority.get(x, 99))
for source in sorted_sources:
articles = by_source[source]
# 按发现时间降序排序(新文章在前)
articles_sorted = sorted(
articles,
key=lambda x: x.get("discovered_at", ""),
reverse=True
)
content.append(f"\n## 📰 {source}\n")
for article in articles_sorted:
emoji = get_article_emoji(article)
title = article.get("title", "Untitled")
url = article.get("url", "")
summary = article.get("summary", "")
content.append(f"### {title}{emoji}")
content.append(f"- **URL**: {url}")
if summary:
content.append(f"- **摘要**: {summary}")
content.append("")
content.append("\n---\n")
content.append(f"\n**生成时间**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
content.append(f"**素材来源**: qiaomu-ai-radar skill")
# 写入文件
index_file = get_weekly_index_path(year, week_num)
index_file.parent.mkdir(parents=True, exist_ok=True)
with open(index_file, "w", encoding="utf-8") as f:
f.write("\n".join(content))
print(f"✅ 生成索引: {index_file}")
print(f" 共 {len(week_articles)} 篇文章,来自 {len(by_source)} 个源")
# 统计emoji
status_count = defaultdict(int)
for article in week_articles:
status = article.get("status", "unread")
status_count[status] += 1
if any(s != "unread" for s in status_count.keys()):
print(f"\n 📊 状态分布:")
for status, count in status_count.items():
if status != "unread":
emoji = EMOJI_MAP.get(status, "")
print(f" {emoji} {status}: {count}")
def generate_current_week():
"""生成当前周的索引"""
now = datetime.now(timezone.utc)
week_num = now.isocalendar()[1]
year = now.year
print(f"🔍 生成第 {week_num} 周索引...")
generate_index_for_week(week_num, year)
if __name__ == "__main__":
try:
generate_current_week()
except Exception as e:
print(f"\n❌ 生成失败: {e}")
import traceback
traceback.print_exc()
#!/usr/bin/env python3
"""
Hacker News 首页热门文章解析器
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "shared"))
from web_utils import fetch_html
def parse_hackernews(source):
"""
解析Hacker News首页,提取前N条热门文章
"""
articles = []
html = fetch_html(source["url"])
if not html:
return articles
# HN的HTML结构:
# <tr class="athing" id="...">
# <td class="title">
# <span class="titleline">
# <a href="...">标题</a>
# 提取所有文章链接和标题
# 使用正则匹配 titleline 中的链接
pattern = r'<span class="titleline"><a href="([^"]+)">([^<]+)</a>'
matches = re.findall(pattern, html)
limit = source.get("limit", 30)
for i, (url, title) in enumerate(matches[:limit]):
# HN的链接可能是相对路径,需要处理
if url.startswith("item?id="):
url = f"https://news.ycombinator.com/{url}"
elif not url.startswith("http"):
url = f"https://news.ycombinator.com/{url}"
articles.append({
"url": url,
"title": title.strip(),
"summary": f"HN排名#{i+1}"
})
return articles
2.12