dsh-xhs-search
给 DeepSeek Harness 的小红书阅读器:搜索、点开、读完一篇笔记,正文和评论落进本地知识库。

装完之后,任何会话里说一句「帮我看看小红书上的 xxx」,模型就会调用 xhs 工具:
拿整屏卡片、点开正文、抓评论、存本地。
零运行时依赖。 不需要 playwright,不需要 puppeteer,不需要 ws——只有 Node ≥ 22.5 的内置 WebSocket 和 fetch。
为什么不是又一个爬虫
小红书的 Web 端能认出自动化。这个工具把四个坑封死在实现里,而不是写在提示词里:
| 坑 | 表现 | 这里的做法 |
|---|---|---|
| 直接跳笔记 URL | 安全限制 300013 / error_code=300031,整个标签页被重定向,之后连搜索都失效 |
只点搜索页的卡片;卡片不在 DOM 里就报 card-not-in-dom,绝不降级成跳 URL |
| CDP 合成点击 | 前几次能用,很快被识别 → 安全限制 / 验证码 | Windows 上走 SetCursorPos + mouse_event 的硬件点击;没有校准时退回 CDP 并明确告诉你降级了 |
| 详情接口 | /api/sns/web/v1/feed 一律 500 |
正文和评论只从 DOM 读 |
| 封面格式 | URL 写着 .jpg,字节其实是 WebP,Word/PDF 不认 |
按魔数嗅探,非 JPEG/PNG 用 Pillow 转码 |
还有一个别人不太提的:详情浮层是"先挂载、后填数据"的。 刚点开的那几百毫秒里标题是空的、点赞数还显示「赞」。这时候读会得到一篇没有正文、没有数字的"成功"笔记—— 比失败更糟。所以读之前会轮询到浮层真的填好为止。
装
// ~/.dsh/profiles/desktop/package.json
{
"dependencies": {
"dsh-xhs-search": "^0.1.0"
},
"dsh": { "profile": { "bundles": ["dsh-xhs-search"] } }
}
然后完全退出 DSH(含托盘图标)再打开。
装完不会自己开浏览器——第一次调用工具才启动。窗口是独立的 --user-data-dir,
和你日常浏览器完全隔离,登录态存在里面。
首次三步
npx xhs-search launch # 起窗口;第一次在这个窗口里扫码登录
npx xhs-search calibrate # 校准视口原点,硬件点击才落得准
npx xhs-search status # 确认登录成功、没有限流、没有验证码
校准只需要做一次。窗口被移动过要重做,否则点击会落偏。
用
工具名 xhs,一个 action 一次调用:
| action | 做什么 |
|---|---|
search |
搜索,返回整屏卡片:标题 / 作者 / 赞藏评 / 封面 / 相对时间 |
read |
从当前搜索页点开一篇,返回正文、话题、配图、最多 60 条评论 |
harvest |
搜完直接开最新的 N 篇,自动入库 |
notes / note |
只读本地知识库,零联网、零风控成本 |
status |
登录门 / 限流 / 验证码 / 本轮预算 |
limits |
现在能不能开下一篇,还要等多久 |
config |
路径与配置 |
命令行是同一套实现,方便人手调:
npx xhs-search search "应援物料" --limit 10
npx xhs-search read 6a9b80c3000000002601fb09
npx xhs-search harvest "追星 物料" --limit 5 --max-age 72
npx xhs-search notes "教程"
限速
平台按账号频次限流,和"是不是真人点击"无关。慢就是快。
| 项 | 默认 |
|---|---|
| 两篇笔记之间 | 25 秒 |
| 每小时上限 | 60 篇 |
| 前 2 篇 | 双倍间隔(热身) |
| 触发限制后的降温 | 20 分钟 × 2^(strikes-1),最长 1 小时上限 |
这些不是建议,是 guard.ts 里的门。xhs limits 说不能开就别开——
绕过它不会更快,只会把「慢一次」变成「封一段」。
三种状态要分清:
login gate— 登录态失效。请人在那个窗口扫码。rate limit— 详情被限流。停手,搜索还能用。captcha— 弹验证码了。只有人能过,任何自动点击都会加重风控。
知识库
默认 ~/Desktop/xhs/:
xhs/
├── browser-profile/ 隔离窗口的登录态
├── osinput-cal.json 视口校准
├── kb/
│ ├── notes.json 笔记:正文 / 作者 / 时间 / 赞藏评 / 话题 / 配图 / 评论
│ ├── searches.json 每次检索记录
│ ├── guard.json 限速状态(重启也记得)
│ └── img/ 配图,<笔记ID>-01.jpg
└── reports/ 留给下游的汇总
notes.json 是原子写:先写临时文件再改名,崩溃不会留下半个文件。
重复读同一篇会合并评论和配图(浮层只渲染第一屏评论,第二次读能看到更多),
但 firstSeen 保留最早那次。
环境变量:XHS_WORKSPACE XHS_BROWSER XHS_PORT XHS_REAL_INPUT XHS_PYTHON
XHS_DETAIL_GAP_MS XHS_SEARCH_GAP_MS XHS_MAX_DETAILS_PER_HOUR XHS_BLOCK_COOLDOWN_MS。
结构
src/
├── types.ts 共享类型
├── config.ts 路径与限速的解析
├── cdp.ts 极小 CDP 客户端(内置 WebSocket/fetch)
├── browser.ts 拉起隔离窗口
├── osinput.ts Win32 硬件点击 + 自校验校准
├── extract.ts 页面脚本(字符串)与纯解析
├── guard.ts 限速与熔断,持久化
├── store.ts 知识库,原子写、合并
├── images.ts 魔数嗅探 + WebP 转码
├── session.ts 领域层:搜索 / 阅读 / 批量
├── tool.ts 模型看到的 xhs 工具
├── cli.ts 人手用的命令行
└── index.ts 插件入口
规则写在领域层而不是提示词里——模型不需要记住,也就没法绕回去。
测试
npm test # 27 个用例:纯逻辑 + 用假 ctx 驱动 apply()
boot.test.mjs 用假 context 跑 apply(),验证:注册一个工具、一个提示词段落、
一个占位符全部替换掉的技能正文,并且启动插件不会开浏览器。
平台
Windows 用于硬件点击(user32.dll)。macOS / Linux 也能跑,但会退回 CDP 合成点击,
风控风险明显更高,read 会明确报 opened by: cdp。
浏览器要 Chromium 系(Edge / Chrome / Brave),Node ≥ 22.5。
红线
- 只读:搜索、读笔记、读评论。不点赞、不评论、不关注、不发帖、不私信。
- 限速:用默认值。高频批量抓取会触发风控,也可能违反平台条款。
- 范围:只处理你有权访问的公开内容;登录态属于你本人,不要导出或分享 cookie。
- 署名:整理内容对外发布时,注明信息来自小红书用户,尊重原作者。
License
MIT
No comments yet. Be the first to write one.