apoorvdarshan/wellfound-bot

GitHub: apoorvdarshan/wellfound-bot

Wellfound/AngeList 求职自动化机器人,结合类人浏览器操控、只读 GraphQL API 捕获与纯 HTTP 重放,实现搜索与批量一键投递。

Stars: 0 | Forks: 0

🎯 wellfound-bot

自动化你自己的 Wellfound 求职过程 —— 搜索每一个过滤器,然后一键自动投递。
一场现代 Web 自动化的实战之旅:类人浏览器控制 → 只读 API 捕获 → 隐形执行任务的纯 HTTP 客户端。

MIT License Python 3.10+ Playwright PRs welcome

**wellfound-bot** 逆向工程了 Wellfound(原 AngelList Talent)的底层运行机制,将你的求职过程变成了一条单一的命令。告诉它*“投递 5 个带薪酬的远程 React 职位”*,它就会解析你的过滤器,执行搜索并投递 —— 无需手动点击。 它涵盖了 **Web 自动化的全栈体系**,因此也可以作为一份学习资源: - 🖱️ **类人浏览器自动化**(Playwright)—— 曲线鼠标轨迹、悬停、思考停顿、随机定时、速率限制。 - 🧩 **只读 API 捕获** —— 通过 CDP 连接到你*自己*真实的 Chrome,并在你浏览时记录网站发出的 GraphQL 调用(`navigator.webdriver` 保持为 false)。 - ⚡ **外部 API 客户端** —— 使用 Chrome TLS/JA3 指纹(`curl_cffi`)通过**纯 HTTP** 重放这些调用。无需浏览器,完全隐形。 - 🧠 **单命令 Agent** —— 基于名称的过滤器(`--skills React`)、全量搜索维度,以及带有上限和延迟的批量自动投递。可通过 Claude 启用可选的自然语言模式。 ## ✅ 证明 —— 一次真实的运行 单次节奏控制的运行成功投递了 **171 / 178** 个标明薪资的远程职位(位于英语国家的公司),且 **DataDome 拦截次数为 0** —— 每份投递都使用了针对特定职位变化的求职信: ![171 of 178 applied, 0 blocked](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/20e0712c76074556.png) ## ⚠️ 首先阅读 —— Wellfound 使用了 DataDome Wellfound 运行了 **DataDome**,这是一款严格的商业反机器人系统。测试显示: - **Headless 模式 = 瞬间触发 CAPTCHA 拦截墙。** Headless 浏览器访问 `/jobs` 会被直接提供 DataDome 的 CAPTCHA,而不是信息流。绝对不要 在这里使用 Headless 模式。 - **真实可见的 Chrome 及你的真实配置文件不会被标记** —— 你 手动登录时没有任何挑战,且对其附加只读连接 可保持 `navigator.webdriver` 为 false。 因此,**最安全的模式是捕获辅助**:*你*操控真实的 Chrome, 工具仅负责记录。没有任何可供 DataDome 捕获的自动化导航。 如果你让工具驱动浏览器(`run.py`),请保持 **Headed 模式**,速度慢、规模小 —— 并承担一定的残余风险。 ## 工作原理 | 文件 | 作用 | |------|------| | `capture_assist.py` | **最安全。** 你浏览真实的 Chrome;工具附加只读连接,并在每次按下 Enter 时记录 DOM + 截图。无自动点击。 | | `login.py` | 打开 Chrome 一次,手动登录;session 保存至 `user_data/`。自动检测登录状态。 | | `run.py` | **风险较高。** 驱动浏览器以类人点击方式遍历职位。仅限 Headed + dry-run 模式。 | | `verify_session.py` | Headed 只读检查,验证保存的 session 能否加载信息流。 | | `record_api.py` | 只读 API 记录器:记录你浏览时 Wellfound 触发的 GraphQL/XHR 调用,用于逆向工程投递 API。 | | `wf_replay.py` | 外部重放客户端:使用 Chrome TLS/JA3 指纹 + 你的 cookies 重新发送捕获到的 API 请求。**风险最高**;默认为 dry-run。 | | `wf_apply.py` | 外部自动投递:`apply` 单个职位或 `batch` 批量投递;串联 `JobApplicationModal` → `CreateJobApplication`,处理 `startupId` + 筛选问题。**风险最高**;默认为 dry-run。 | | `wf_search.py` | 外部职位搜索:过滤器 + 分页,通过标签 ID **或名称**(`--skills React`)。默认进行干净过滤;使用 `--from-capture` 对你的网站过滤器进行分页。只读。 | | `wf_resolve.py` | 通过自动补全 API(技能/市场/地点)解析过滤器名称 → 标签 ID。 | | `wf_agent.py` | 一键 Agent:按名称/过滤器搜索 → 自动投递(批量)。目前支持常规参数;如果安装了 `anthropic` + `ANTHROPIC_API_KEY` 则支持自然语言 `--query`。 | | `config.py` | 包含你的搜索 URL、批量大小、延迟、dry-run 开关。 | | `wellfound/human.py` | “正确点击”的逻辑 —— 移动、悬停、定时。 | | `wellfound/browser.py` | 持久化的真实 Chrome 配置文件,最小化/一致的指纹。 | | `wellfound/capture.py` | 将内容写入 `captures//flow.jsonl` 及每一步的 `.html` / `.png`。 | ## 推荐:捕获辅助(安全) ``` python capture_assist.py ``` 一个真实的 Chrome 窗口将以你的 `user_data/` 配置文件打开。正常浏览 Wellfound —— 搜索、打开职位、打开投递弹窗。每当你要 记录某一步时,切换到终端并按下 **Enter**;输入 **q** 结束。 每次捕获都会将 DOM + 截图保存到 `captures/assist-/`,构建出你提供给 Agent 的 `flow.jsonl` 追踪记录。DataDome 看到的永远只是 你正常的手动浏览行为。 ## 逆向工程 API(只读) ``` python record_api.py # then browse + apply by hand, Ctrl-C to stop ``` 这会记录*你*使用时 Wellfound 发出的每一个 GraphQL/XHR 请求 + 响应, 并将其存入 `captures/api-/requests.jsonl`(外加一个被 gitignored 的 `cookies.json`)。这是*查看*投递 API 的安全方法。 **不要从普通脚本中重放它。** Wellfound 的 DataDome 也会 保护该 API,并对 TLS 握手(JA3)及绑定的 `datadome` cookie 进行指纹验证 —— 包含你 cookies 的 Python `requests` 调用拥有非 Chrome 的 TLS 指纹,往往比 浏览器*更快*被拦截。如果你想操作该 API,安全的路径是 在**真实的 Chrome 内部**运行 `fetch()` (正确的 TLS + 实时的 `datadome` cookie + session),而不是使用外部客户端。 ### 外部重放(`wf_replay.py`)—— 风险最高 如果你仍然选择外部路线,最稳妥的做法是 使用 Chrome TLS 指纹重放 Wellfound *真实*捕获的请求(而非猜测)。流程如下: ``` python record_api.py # capture: do ONE manual apply, Ctrl-C python wf_replay.py list # find the apply request's index python wf_replay.py replay --index 7 # DRY-RUN: prints what it would send python wf_replay.py replay --index 7 \ --set variables.jobId=12345 --send # actually send (for a new job id) ``` `wf_replay.py` 会模拟 Chrome 的 JA3(`curl_cffi`),加载你捕获的 cookies(包含 `datadome`),并重放捕获到的 headers + body,使用 `--set` 来编辑 GraphQL 变量。默认处于 dry-run 模式;`--send` 会触发 发送。如果 DataDome 检测到外部客户端,它会返回一个 CAPTCHA/`datadome` 响应体,脚本会对此进行标记。**保持极小的请求量** —— 这是最容易 导致账号被标记的模式。 ### 外部自动投递(`wf_apply.py`) 比原始重放级别更高:只需给它一个职位 ID,它就会自动完成这两步 流程。 ``` python wf_apply.py jobs # job ids found in the capture python wf_apply.py apply --job 4174674 # DRY-RUN: reads modal, prints the plan python wf_apply.py apply --job 4174674 --note "Keen to contribute" --send python wf_apply.py apply --job 4174674 --answer 263758="..." --send # answer a screening Q ``` 它会获取 `JobApplicationModal`(只读)以解析 `startupId` 以及任何 筛选问题,如果有**必填**问题未作答则拒绝投递, 然后发送 `CreateJobApplication`。已针对 Wellfound 进行验证: 外部客户端会收到 HTTP 200 的投递成功响应(而非 DataDome 拦截)。但这仍是风险最高的模式 —— 请缓慢地少量投递,并在 签名失效时重新捕获。 ### 搜索 + 过滤器(`wf_search.py`)与完整 pipeline ``` python wf_search.py # paginate the captured filter python wf_search.py --max-pages 5 --exclude-applied --remote REMOTE_OPEN --salary-min 100000 python wf_search.py --role-tags 157714,103480 --location-tags 2203 ``` 每个 Wellfound 过滤器都映射到一个命令行参数(对应服务器端的真实字段): - **维度(标签 ID):** `--role-tags`, `--skill-tags`, `--market-tags`, `--location-tags`, `--remote-company-tags` - **值:** `--job-types`, `--remote`, `--keywords`, `--exclude-keywords`, `--company-sizes`(`SIZE_1_10`…), `--investment-stages`(`SEED_STAGE`, `SERIES_A`…), `--salary-min/--salary-max`, `--currency`, `--equity-min/--equity-max`, `--years-min/--years-max` - **开关:** `--mostly-remote`, `--responsive`, `--visa`, `--hide-external`, `--include-no-salary` - **排序:** `--sort recommended|recent|active` (→ `sortBy`) - **分页/便捷参数:** `--page`, `--max-pages`, `--exclude-applied`, `--native-only`, `--ids-only` **基于标签的维度(职位/技能/市场/地点)使用的是 ID,而不是名称。** 设定它们最简单的方法是在 `record_api.py` 记录时在 wellfound.com 上应用你的过滤器 —— 捕获结果随后会保存你的精确过滤器,`wf_search` 会对 其进行分页,并支持通过参数在此基础上微调任何字段。 链式搜索 → 批量投递(带上限与间隔;除非使用 `--send`,否则为 dry-run): ``` python wf_search.py --ids-only --exclude-applied | python wf_apply.py batch --max 5 --delay 45 --send ``` ### Agent —— 单命令搜索 + 投递(`wf_agent.py`) 一键完成所有操作。按**名称**过滤(解析为标签 ID), 使用干净的过滤器搜索,保留原生的且未投递过的职位,并 批量投递 —— 除非使用 `--send`,否则为 dry-run: ``` # flags(现已可用,无需 API key): python wf_agent.py --skills React,Node --remote --locations "San Francisco" --max 5 # dry-run python wf_agent.py --skills React --remote --max 5 --send # apply for real # natural language(需要:pip install anthropic + ANTHROPIC_API_KEY): python wf_agent.py --query "remote react jobs in SF, seed/series-A, apply to 5" --send ``` 如果没有 API key,自然语言层可以直接交给 **Claude Code 本身** —— 告诉它你的条件,它就会运行相同的 `wf_resolve` → `wf_search` → `wf_apply` pipeline。职位是一个固定的 Wellfound 列表(无 预输入提示),因此请使用**技能**或**关键词**来指代它们。 ## 设置 ``` cd wellfound-bot python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt playwright install chromium # one-time browser download ``` ## 1. 登录(只需一次) ``` python login.py ``` 一个真实的 Chrome 窗口将打开。按照你平时的方式登录(电子邮件、 Google、魔法链接)。当你看到仪表盘时,在 终端按下 Enter。你的 session 将被保存至 `user_data/`。 ## 2. 捕获一次运行 首先编辑 `config.py` —— 至少粘贴你过滤后的 `JOBS_URL`。 暂时将 `DRY_RUN` 设置为 `True`。 ``` python run.py ``` 它会打开你的职位信息流,最多遍历 `MAX_JOBS_PER_RUN` 个职位,通过类人点击 打开每个投递表单,并在**提交前停止**。 所有内容都保存在 `captures//` 下。 ## 3. 将其提供给 Agent 每次运行都会产生: - `flow.jsonl` —— 每步对应一个 JSON 行:`action`、`url`、`selector`、 `title`,以及匹配的 `screenshot` / `html` 文件名。 - `NNN.html` —— 该步骤的完整 DOM(用于查找/修复选择器)。 - `NNN.png` —— 该步骤的截图。 `flow.jsonl` 是提供给 Agent 的结构化追踪记录 —— 它描述了 确切的动作序列以及哪个选择器产生了每个页面,而 HTML 则让 Agent 能够推断下一步操作或修复选择器。 ## 实际投递 当捕获结果看起来没问题后,在 `config.py` 中设置 `DRY_RUN = False`(并 可选地配置 `DEFAULT_MESSAGE`)。现在 `run.py` 也会点击提交。请保持 `MAX_JOBS_PER_RUN` 在较小的数值,每天运行几次,而不是进行一次巨大的 批量投递。 ## 保持不被检测 伪装的核心在于**使用真实的 Chrome,而不是被篡改过的。** 该 bot 使用 你安装的 Chrome 二进制文件和持久化的配置文件,关闭 自动化标志(从而使 `navigator.webdriver` 保持其自然的 `false` 状态),并且 不对其指纹进行任何*干预*。它故意**不** 伪造 User-Agent、插件、语言或时区 —— 这些内容会 与 Chrome 真实的 User-Agent Client Hints 不一致,从而成为 破绽。输入是基于人类节奏的(曲线连续鼠标移动、真实的按键 时长、逐字符输入、随机思考时间),并且运行受到 速率限制和上限约束。运行时请保持 Headed 模式以获得最干净的指纹。 ## 测试 - `python smoke_test.py —— 检查浏览器、伪装掩码、人类辅助行为 和捕获功能(无需登录)。 - `python flow_test.py` —— 在模拟 Wellfound 信息流 + 投递弹窗的本地 页面上,驱动 `run.py` 执行真实的流程逻辑(无需登录,无网络请求)。 ## 注意事项与限制 - **选择器已尽力做到最好。** Wellfound 的 DOM 会发生变化;如果某一步 报告“未找到 Apply 按钮”,请打开该步骤的 `.html` 以查找当前的 选择器,并更新 `run.py` 顶部的列表。 - **请尊重 Wellfound 的服务条款。** 本工具旨在用于从 *你自己的*账号自动化投递*你自己的* 职位申请。请勿进行大规模 抓取或操作不属于你的账号。 - **`user_data/` 保存了你实时的登录状态。** 它已被 gitignored —— 切勿提交 或分享它。 ## License [MIT](LICENSE) © Apoorv Darshan
标签:GraphQL, Playwright, Python, RPA, Web自动化, 无后门, 特征检测, 自动化求职, 逆向分析, 逆向工具