$npx -y skills add xiaomoBoy/claude-writing-skills --skill research-collectorUse when the user wants to collect research material for an article or topic by gathering YouTube videos and web articles into a NotebookLM notebook, then running analysis queries and saving results as markdown. Best for "收集素材"、"给我找这个话题的相关视频和文章"、"整理成 NotebookLM 分析" type requests.
| 1 | # Research Collector |
| 2 | |
| 3 | 这个 skill 只做一件事: |
| 4 | |
| 5 | - 为某个主题批量收集 YouTube 视频 + 网页文章,喂进 NotebookLM,跑分析查询,把结果落地到本地目录(默认 `./research/<topic>/`,可配置) |
| 6 | |
| 7 | 不负责: |
| 8 | |
| 9 | - 写成品文章(交给你自己的写作工具 / skill) |
| 10 | - 选主标题 |
| 11 | - 下载视频(交给本仓库里的 `yt-dlp-direct` skill) |
| 12 | - 发布到多平台(交给本仓库里的 `publisher-wechatsync` skill) |
| 13 | |
| 14 | 一句话原则:用户说"帮我收集 X 话题的素材"或"拉一批 YouTube + 文章到 NotebookLM",就走这条固定流水线,不要每次重新设计。 |
| 15 | |
| 16 | ## When To Use |
| 17 | |
| 18 | 适用场景: |
| 19 | |
| 20 | - 用户要为某个话题写推荐/测评/观点文,需要先做背景研究 |
| 21 | - 用户说"帮我找 X 的热门 YouTube 视频和文章" |
| 22 | - 用户说"收集到 NotebookLM 里分析" |
| 23 | - 用户说"给我整理一份 X 话题的素材研究" |
| 24 | |
| 25 | 不适用场景: |
| 26 | |
| 27 | - 用户已经有明确素材清单,只想要总结 → 直接跑 `nlm notebook query` |
| 28 | - 用户要做的是实时对话研究,不需要持久化到 notebook → 用 WebSearch + WebFetch |
| 29 | - 用户只要下载单个视频 → 用 `yt-dlp-direct` |
| 30 | |
| 31 | ## Preconditions |
| 32 | |
| 33 | 开始前必须确认: |
| 34 | |
| 35 | 1. `nlm` CLI 已安装且登录:`nlm login --check` |
| 36 | 2. `yt-dlp` 在 PATH 中:`which yt-dlp` |
| 37 | 3. 用户明确说明了主题和角度 |
| 38 | 4. 输出目录可写(默认 `./research/<topic>/`,可以通过 `RESEARCH_OUTPUT_DIR` 环境变量或对话里直接指定其他路径) |
| 39 | |
| 40 | 前置不满足时: |
| 41 | |
| 42 | - `nlm login --check` 失败 → 让用户跑 `nlm login`,session 有效期 ~20 分钟 |
| 43 | - `yt-dlp` 没装 → 停止并告诉用户 |
| 44 | |
| 45 | ## Working Rules |
| 46 | |
| 47 | - 先和用户对齐主题、角度、量级,再动手 |
| 48 | - 每轮 ytsearch 默认 15 条,可以根据需要调整 |
| 49 | - NotebookLM deep research 一次只能跑一个任务,不能并发 |
| 50 | - 添加 source 时每条之间 sleep 2 秒,避免限流 |
| 51 | - 所有产出(原始 JSON + 汇总 markdown)落到 `./research/<topic>/` 下(或用户指定的目录) |
| 52 | - 这个 skill 只负责收集和分析,不要擅自接着写成品文章 |
| 53 | - 不要删 notebook,用户后面可能还要回去跑 query |
| 54 | |
| 55 | ## Core Workflow |
| 56 | |
| 57 | ### Phase 0: 对齐目标 |
| 58 | |
| 59 | 在动手前必须和用户明确: |
| 60 | |
| 61 | 1. **主题是什么**(要一句话能喂给 ytsearch 的关键词) |
| 62 | 2. **角度**(比如"最常用 + 个人创作" vs "最新发布 + 技术细节") |
| 63 | 3. **笔记本命名**(默认 `<主题> 素材`) |
| 64 | 4. **量级**(默认:15 油管 + deep research 自动 ~40 网页) |
| 65 | |
| 66 | ### Phase 1: 创建笔记本 + 设 alias |
| 67 | |
| 68 | ```bash |
| 69 | nlm notebook create "<话题> 素材" |
| 70 | # 从输出提取 ID,然后: |
| 71 | nlm alias set <short-name> <notebook-id> |
| 72 | ``` |
| 73 | |
| 74 | alias 取短名,比如 `skills-research`、`vps-2026`,后续所有命令都用 alias。 |
| 75 | |
| 76 | ### Phase 2: yt-dlp ytsearch 找热门 YouTube |
| 77 | |
| 78 | 并行跑 2-3 个不同角度的搜索,每个 15 条: |
| 79 | |
| 80 | ```bash |
| 81 | yt-dlp --simulate --print "%(title)s|%(webpage_url)s|%(view_count)s|%(uploader)s" \ |
| 82 | "ytsearch15:<关键词 A>" |
| 83 | yt-dlp --simulate --print "%(title)s|%(webpage_url)s|%(view_count)s|%(uploader)s" \ |
| 84 | "ytsearch15:<关键词 B>" |
| 85 | ``` |
| 86 | |
| 87 | 输出里的 JS runtime warning 可以忽略。 |
| 88 | |
| 89 | 从结果里按以下规则筛 top 15: |
| 90 | |
| 91 | - 去重(同一视频出现在多个搜索里) |
| 92 | - 优先官方账号(比如 Anthropic、OpenAI 等) |
| 93 | - 按 view count 从高到低,但要留 2-3 个垂直向角度的中腰部视频,避免全是爆款通稿 |
| 94 | - 每个角度至少保留 5 条 |
| 95 | |
| 96 | ### Phase 3: 把 YouTube 加为 source |
| 97 | |
| 98 | 用 bash 循环逐条加,每次 sleep 2 秒: |
| 99 | |
| 100 | ```bash |
| 101 | cat > /tmp/yt_urls.txt <<'EOF' |
| 102 | https://www.youtube.com/watch?v=XXX1 |
| 103 | https://www.youtube.com/watch?v=XXX2 |
| 104 | ... |
| 105 | EOF |
| 106 | |
| 107 | while IFS= read -r url; do |
| 108 | echo "=== Adding: $url ===" |
| 109 | nlm source add <alias> --url "$url" 2>&1 | tail -5 |
| 110 | sleep 2 |
| 111 | done < /tmp/yt_urls.txt |
| 112 | ``` |
| 113 | |
| 114 | 偶尔会遇到单条失败(视频不公开、区域限制),忽略继续,最后报告成功率。 |
| 115 | |
| 116 | ### Phase 4: 跑 NotebookLM deep research 发现网页文章 |
| 117 | |
| 118 | ```bash |
| 119 | nlm research start "<英文查询,适合 web 研究>" \ |
| 120 | --notebook-id <alias> --mode deep |
| 121 | ``` |
| 122 | |
| 123 | deep 模式 ~5 分钟,返回 ~40 条网页源。 |
| 124 | |
| 125 | **关键:一个 notebook 同一时间只能有一个 research 任务在跑**。如果要跑第二轮,必须等第一轮 import 完或 --force。 |
| 126 | |
| 127 | 等待完成: |
| 128 | |
| 129 | ```bash |
| 130 | nlm research status <alias> --max-wait 360 |
| 131 | ``` |
| 132 | |
| 133 | Bash 工具默认 timeout 120 秒,必须加 `timeout: 400000`(即 400 秒)。 |
| 134 | |
| 135 | ### Phase 5: 导入 research 结果 |
| 136 | |
| 137 | 研究完成后从输出里拿 task-id,然后: |
| 138 | |
| 139 | ```bash |
| 140 | nlm research import <alias> <task-id> --timeout 600 |
| 141 | ``` |
| 142 | |
| 143 | Bash 工具加 `timeout: 700000`。 |
| 144 | |
| 145 | **注意**:用户有时会说"素材够了,不用再导入",要停下来直接进 Phase 6。 |
| 146 | |
| 147 | ### Phase 6: 跑 3 个分析查询 |
| 148 | |
| 149 | 默认跑 3 个角度,命令直接重定向到文件避免输出过大: |
| 150 | |
| 151 | ```bash |
| 152 | mkdir -p "./research/<topic>" |
| 153 | |
| 154 | nlm notebook query <alias> "<问题 1 的中文提示>" \ |
| 155 | > "./research/<topic>/query1-<slug>-raw.json" 2>&1 |
| 156 | |
| 157 | nlm notebook query <alias> "<问题 2 的中文提示>" \ |
| 158 | > "./research/<topic>/query2-<slug>-raw.json" 2>&1 |
| 159 | |
| 160 | nlm notebook query <alias> "<问题 3 的中文提示>" \ |
| 161 | > "./research/<topic>/query3-<slug>-raw.json" 2>&1 |
| 162 | ``` |
| 163 | |
| 164 | **每个 query 的 Bash 调用要加 `timeout: 240000`。** |
| 165 | |
| 166 | 默认 3 个查询模板(按需改关键词): |
| 167 | |
| 168 | 1. **Top 清单**:"基于所有 source,请列出被最多来源推荐的 Top 10 X。对每个 X 说明:(1) 名称 (2) 具体做什么 (3) 主要使用场景 (4) 推荐它的来源数量 (5) 类型分类。按推荐频率从高到低,用中文输出。" |
| 169 | 2. **目标读者向**:"我要写一篇面向 <读者画像> 的文章。请筛选出对 <读者> 最有帮助的 Top 8 X,每个说明:(1) 名称 (2) 具体痛点 (3) 典型用法一句话 (4) 类型 (5) 最具体的来源编号。去掉不相关的,聚焦 <场景>,用中文。" |
| 170 | 3. **入门 + 坑**:"针对 <读者> 使用 X 时,请总结:(1) 最快入门方式 (2) 去哪里获取 (3) 最容易踩的 5 个坑 (4) 什么时候其实不需要 (5) 最新的重要更新。每点配来源编号,用中文。" |
| 171 | |
| 172 | ### Phase 7: 抽取 answer 字段,生成汇总 markdown |
| 173 | |
| 174 | 原始输出是 JSON 包含 answer + citations,用 Python 抽 `value.answer` 字段: |
| 175 | |
| 176 | ```bash |
| 177 | python3 <<'PY' |
| 178 | import json, pathlib |
| 179 | base = pathlib.Path("./research/<topic>") |
| 180 | files = [ |
| 181 | ("query1-<slug>-raw.json", "## Query 1:<标题>"), |
| 182 | ("query2-<slug>-raw.json", "## Query 2:<标题>"), |
| 183 | ("query3-<slug>-raw.json", "## Query 3:<标题>"), |
| 184 | ] |
| 185 | out = ["# <话题> 素材研究", "", |
| 186 | "> 基于 NotebookLM 笔记本 `<notebook-name>` 的分析结果", "", |
| 187 | "---", ""] |
| 188 | for fname, heading in files: |
| 189 | out.append(heading) |
| 190 | out.append("") |
| 191 | raw = (base/fname).read_text() |
| 192 | try: |
| 193 | data = json.loads(raw) |
| 194 | out.append(data.get("value",{}).get("answer","")) |
| 195 | except Exception as e: |
| 196 | out.append(f"(解析失败: {e})") |
| 197 | out.append("") |
| 198 | out.append("---") |
| 199 | out.append("") |
| 200 | (b |