知识星球文章下载 Skill
批量下载某个知识星球(wx.zsxq.com/group/<group_id>)的全部主题文章,保存为 Markdown 文件,文件名 日期-标题-作者.md,按年份归档到指定目录。
核心思路(已实测验证的稳定路径):不用 API 分页(zsxq 的 /v2/groups/.../topics 接口 count 参数不稳定、end_time 分页易空),改用 DOM 无限滚动采集——通过 browser-skill 在已登录页面上反复滚动加载 + 解析 .topic-container,把结果持久化到 localStorage,最后一次性导出。详见 references/zsxq-notes.md。
依赖与前置条件
- browser-skill(bsk)已安装并可用(
bsk status能看到 daemon + 至少 1 个浏览器已连接)。 - 本机 Chrome/Edge 已登录目标星球(能在浏览器里正常打开星球页面看到帖子列表)。
- Python 3(用于落盘服务与 Markdown 转换脚本,用
python或绝对路径调用)。
> 若 bsk 未安装:bsk install-skill --harness workbuddy 自动装入;CLI 用官方 install.ps1。若浏览器未登录:先在浏览器手动打开星球并登录,再回来执行。
可配置项(执行前必须确定)
| 配置项 | 说明 | 默认/示例 |
|---|---|---|
星球 URL 或 group_id |
目标星球。从 https://wx.zsxq.com/group/88885814145422 提取 group_id(路径末尾那串数字) |
用户提供 |
星球名称 |
用于归档目录名与日志。可从页面标题(如「毯叔金融密友圈-知识星球」)取,或用户指定 | 自动取标题 |
存放路径 |
Markdown 落盘根目录。脚本会在此下建「年份」子目录 | 用户提供(如 D:\毯叔的AI知识库\毯叔金融密友圈) |
采集批次大小 |
每次 bsk evaluate 的滚动次数,避免单次超时 |
8 次/批(约 45s 超时) |
执行前先问清「星球 URL」和「存放路径」;用户给了 URL 但没给路径时,用 D:\<星球名称>\<星球名称>\ 之类的合理默认并告知。
工作流
第 1 步:确认前置状态
bsk status # 看 daemon 是否运行、浏览器是否连接
bsk browsers # 拿到浏览器 INSTANCE id(如 41f89a19)
daemon 未运行或浏览器未连接时,先让用户启动/连接(或 bsk session start --browser <INSTANCE> 拉起)。
第 2 步:建立会话并打开星球页面
bsk session start --browser <INSTANCE> # 返回 session id(如 pwyt)
bsk tab list --session <SESSION> # 找用户已打开的星球 tab(TITLE 含「知识星球」)
- 优先 borrow 用户已登录的 tab(
bsk tab borrow <tab_id> --session <SESSION>),复用登录态,最省事。 - 若没有现成 tab:
bsk tab create --url "https://wx.zsxq.com/group/<group_id>" --session <SESSION>新建,等页面加载(sleep 5后bsk snapshot确认)。
第 3 步:切到「最新」标签(关键,否则只采到精华帖)
星球页默认可能停在「精华」标签,会漏掉大量帖子。执行:
bsk evaluate --session <SESSION> --tab-id <TAB_ID> \
"(function(){const tabs=[...document.querySelectorAll('div.item')];const t=tabs.find(b=>(b.innerText||'').trim()==='最新');if(t){t.click();return 'clicked 最新'}return 'not found'})()"
然后 sleep 4 后验证激活标签与首帖日期是否为今天。
第 4 步:反复执行批次采集(核心循环)
读取 scripts/extract.js 的内容,把其中 <KEY> 替换为唯一 key(建议 __zsxq_topics)、<BATCH_SCROLLS> 设为 8、<SCROLL_WAIT_MS> 设为 1800,然后原样作为字符串参数传给:
bsk evaluate --session <SESSION> --tab-id <TAB_ID> --timeout 60s "<替换后的脚本>"
脚本是幂等批次:每次执行滚动一批、把新帖累加进 localStorage[KEY] 去重,返回 {done, total, newThisBatch, oldest, newest}。循环执行,直到返回 done:true(或连续多批 newThisBatch:0 且页面已到底)。
要点:
- 每批之间间隔 1~2 秒,避免把页面/daemon 打崩。
- 若返回超时,用「少滚动批次」重试(把 BATCH_SCROLLS 降到 5~6),状态在 localStorage 里不会丢。
- 想随时看进度:bsk evaluate ... "(function(){const d=JSON.parse(localStorage.getItem('<KEY>')||'[]');return JSON.stringify({total:d.length})})()"。
第 5 步:导出 JSON 到本地(绕过浏览器下载拦截)
浏览器里 Blob + a.click() 下载常被拦截,用本地 HTTP 服务落盘:
- 启动服务(后台):
bash python scripts/save_server.py "<输出json路径>" <端口> - 在页面里把
localStorage[KEY]POST 过去:
bash bsk evaluate --session <SESSION> --tab-id <TAB_ID> --timeout 120s \ "(async()=>{const data=JSON.parse(localStorage.getItem('<KEY>')||'[]');const r=await fetch('http://127.0.0.1:<端口>/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});return JSON.stringify(await r.json())})()" - 停掉服务,验证 JSON 文件已生成且条数正确。
> 注意:本地服务需在本机可访问;bsk 驱动的页面与 Python 服务同机,127.0.0.1 直接可达。若被代理拦,curl 侧加 --noproxy '*'。
第 6 步:转换为 Markdown
python scripts/to_markdown.py "<输入json路径>" "<存放路径>"
脚本会:去重 → 提取标题(优先 【...】 内文字,否则正文首行)→ 生成 日期-标题-作者.md → 按年份建子目录归档 → 文件名冲突自动追加序号 → 生成 _README.md(含总数/年份分布/作者统计)。
第 7 步:验证与交付
find "<存放路径>" -name "*.md" | wc -l # 总数
ls -lt "<存放路径>/<最新年份>" | head # 抽查最新几篇
向用户交付摘要表格:总数、年份分布、存放路径、命名规则、抽查样例。若采集总数明显少于星球宣称的主题数,检查是否漏了「最新」标签或未滚动到底。
跨平台使用说明
- WorkBuddy:本 skill 直接可用(browser-skill 官方支持 workbuddy harness)。
- OpenClaw / 其他 Agent 环境:SKILL.md 是通用格式,Agent 读本文件后按上述
bsk命令 + 脚本执行即可;scripts/下脚本均为纯 Python / 浏览器内 JS,不依赖特定框架。若目标环境用 Codex 兼容格式,可参照本 skill 生成openai.yaml并$name引用(同目录脚本路径保持不变)。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 只采到几百条 | 停在「精华」标签 | 第 3 步切「最新」 |
bsk evaluate 每次重新计数 |
没用 localStorage 持久化 | 脚本必须用 localStorage,别用 window 全局变量 |
| 单批超时 | 滚动次数太多 | 降 BATCH_SCROLLS,或 --timeout 调到 60s |
| 下载被浏览器拦截 | Blob 下载被拦 | 第 5 步改用本地 HTTP 服务 |
| 文件名覆盖 | 同名帖 | to_markdown.py 已自动追加序号 |
| 代理截断 | 本机 HTTP/HTTPS 代理 | 联网命令加 --noproxy '*' 或先 unset HTTPS_PROXY HTTP_PROXY |