1实现原理 · 为什么它能做到
入口是一个 shell 包装 + vendored 的 TS CLI:baoyu-fetch 直接 exec bun 跑 lib/cli.ts,CLI 再按 URL 选适配器(x / youtube / hn / generic)。
exec bun "$script_dir/lib/cli.ts" "$@"
抓取靠真实 Chrome + 手写 CDP 客户端(chrome-launcher 起浏览器,ws 直连 DevTools),不是 puppeteer。
import WebSocket from "ws";
X 适配器不是只读 DOM,而是内置会话机制:先用 CDP 读 x.com/twitter.com cookie(要求 auth_token 与 ct0),把 cookie 明文写进 profile 目录下的 sidecar 文件,下次运行再回灌。
await writeFile(filePath, JSON.stringify(data, null, 2));
X 的数据来源是页面自己发出的 GraphQL 响应:工具用 CDP 网络日志把 /graphql/ 的 TweetDetail / TweetResultByRestId 等响应体捞出来再解析,还会自动滚动+点『Show replies』加载长贴。
entry.url.includes("TweetDetail") ||
YouTube 适配器是自建 InnerTube 调用:从页面 ytcfg 取 INNERTUBE_API_KEY,在页面上下文 POST /youtubei/v1/player(credentials: 'include',带页面 cookie),并显式冒充 ANDROID 客户端。
context: { client: { clientName: 'ANDROID', clientVersion: '20.10.38' } },
可复用已运行的 Chrome 调试会话:先扫 ps aux 找带同一 profile 且带 --remote-debugging-port 的进程,或读取 profile 里的 DevToolsActivePort 文件,找到就连上。
const activePort = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));
遇到登录墙/CAPTCHA 不是硬闯,而是提供等待模式:检测到 Cloudflare/reCAPTCHA 等在页面出现时,弹出可见 Chrome 让用户手动过,检测到进展或按回车就继续。
text.includes("verify you are human") ||
生成 markdown 的主链是本地 Defuddle/Readability + 质量评分,质量不足时才把目标 URL 交给第三方服务 https://defuddle.md 取更干净的 markdown。
const DEFUDDLE_API_ORIGIN = "https://defuddle.md";
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | @mozilla/readability ^0.6.0(正文提取) |
| package | defuddle ^0.17.0(本地 markdown 提取) |
| package | jsdom ^30.0.1(HTML 清洗/解析,未开 runScripts) |
| package | turndown + turndown-plugin-gfm(HTML→markdown) |
| package | chrome-launcher ^1.2.1(浏览器启动) |
| package | ws ^8.18.3(CDP WebSocket 客户端) |
| package | unified + remark-parse + remark-gfm(媒体链接收集与重写) |
| cli | bun(CLI 运行时;shell 包装 exec bun) |
| cli | Chrome/Chromium 二进制(chrome-launcher,可用 --browser-path 指定) |
| cli | ps aux(POSIX 进程扫描)与 osascript(macOS 窗口激活) |
| network | Chrome DevTools HTTP/WebSocket 端点 |
| api | 第三方 markdown 服务 defuddle.md(质量不足时把目标 URL 发过去) |
| network | YouTube watch 页、InnerTube player、caption 轨与 i.ytimg.com 缩略图 |
| network | Hacker News 条目页(jsdom 解析) |
| network | X/Twitter 页面与页面自身发出的 /graphql/ 请求(工具只观察,不构造请求) |
| network | 用户指定 URL 与页面内媒体 URL(下载时带伪造的桌面 Chrome UA) |
4风险提醒 风险提醒:红色 · 谨慎使用
- X 会话 cookie 明文落盘(判红要素之一) — auth_token/ct0 等会以明文 JSON 写在 Chrome profile 目录的 x-session-cookies.json,无加密与权限收紧;共享机器或备份同步目录下需格外注意,用后应清理。
- 逆向/冒充 YouTube 客户端(判红要素之一) — 页面内 InnerTube 调用 + ANDROID 客户端冒充,属绕过平台检测的路线;随 YouTube 风控变化而失效,也可能触发账号侧限制。
- 接管已运行的 Chrome 调试会话 — 会连上你正在用的调试实例(DevToolsActivePort / ps aux / --cdp-url);被接管的浏览器可能被用来导航与点击(如自动点 Show replies、滚动)。
- 调试落盘可能包含敏感数据 — --debug-dir 会把登录后页面 HTML 与带请求头/响应体的网络日志写到磁盘,可能含 token 与私人内容,分享前务必检查。
- 目标 URL 会发给第三方服务 — markdown 模式且本地质量不足时,目标 URL 被送到 https://defuddle.md;内网/带 token 的链接不宜走该兜底。
- 文档与实现滑移导致预期错位 — 『默认无头』与『默认 profile 相对路径』两处均与代码不符,cookie 导出更是完全未披露——使用前应以源码为准。
5第二遍独立确认
- [ok] YouTube 适配器的逆向/冒充 — transcript.ts 有 window.ytcfg?.data_?.INNERTUBE_API_KEY、页面内 POST /youtubei/v1/player?key=…(credentials: 'include')与 context.client.clientName='ANDROID' 三处,与 Main 终裁的『逆向 youtubei + 冒充官方客户端』一致。
- [ok] X cookie 明文 sidecar 落盘 — cookie-sidecar.ts 用 Network.getCookies 取 cookie 后 JSON.stringify 明文写入 <profile>/x-session-cookies.json(x/index.ts 指定 filename),无加密无 chmod;convert.ts 在开头尝试回灌、finally 里导出。
- [ok] 接管已运行调试会话 — profile.ts 解析 DevToolsActivePort 并 spawnSync('ps', ['aux']) 匹配 profileDir + --remote-debugging-port;chrome-launcher.ts 支持 --cdp-url ws:// 直连与复用日志 'Reusing Chrome debugger at …'。
- [ok] 网络日志含请求头与响应体(--debug-dir 落盘) — network-journal.ts 记录 requestHeaders / requestBody / 响应体(Network.getResponseBody),convert.ts 在写 debug 前调用 toJSON({ includeBodies: true }) 并写入 network.json。
- [ok] 第三方 defuddle.md 兜底 — DEFUDDLE_API_ORIGIN=https://defuddle.md,构建 URL 后 fetch;通用适配器仅在 outputFormat==='markdown' 时启用该兜底,JSON 模式不会外发目标 URL。
- [discrepancy] 文档与实现不一致(默认无头) — SKILL.md 第 93 行写 '# Default: headless capture, markdown to stdout'、quality-gate.md 亦写 'Default | Headless Chrome',但 cli.ts 默认 headless: false(convert.ts 在非交互模式传该值),实际会启动可见 Chrome(--no-startup-window)。使用前应知道默认并非无头。
- [discrepancy] 文档与实现不一致(默认 profile 路径) — SKILL.md 写默认 ./baoyu-skills/chrome-profile,代码实际解析为按 OS 的绝对路径(XDG_DATA_HOME / Application Support / APPDATA 下的 baoyu-skills/chrome-profile),从不使用 cwd 相对路径。
- [discrepancy] 文档未披露 cookie 导出 — SKILL.md 只写 BAOYU_CHROME_PROFILE_DIR 为『Chrome user data directory』,从未提及会导出 X cookie 到 sidecar 文件;该行为只在代码中可见——这是本次侦查中风险披露落差最大的一处。
6结论
15be54497a8d6c30…1567581c26