
Ethan Collins
Pattern Recognition Specialist

Cloudflare 的机器人防护远远超出 CAPTCHA 小组件的范畴。Cloudflare Challenge — 即覆盖整个页面的“正在验证您的浏览器…”屏幕,会完全阻止访问网站 — 是网络上最激进的机器人防御之一。它在真实的浏览器环境中运行完整的 JavaScript 挑战,检查行为信号,并在放行之前对您的 TLS 连接进行指纹识别。
标准的自动化工具在这里会失败。原因不是它们无法解决挑战,而是 Cloudflare 对 TLS 握手本身进行指纹识别 — 任何非浏览器的 HTTP 客户端会在挑战页面出现之前被拦截,或者即使带有有效的 cf_clearance cookie,也会被立即重新挑战。
在本指南中,您将学习如何在 n8n 中构建一个真正可用的Cloudflare Challenge 爬虫 — 结合使用**CapSolver** 解决挑战,使用一个本地 Go TLS 服务器伪装成 Chrome 的 TLS 指纹,并通过 n8n 工作流将它们整合在一起。
您将构建的内容:
Cloudflare Challenge(也称为 JS 挑战或机器人管理挑战)是 Cloudflare 在访客访问受保护网站之前注入的全页面插页。您一定见过:一个黑色或白色的屏幕,上面有“正在验证您的浏览器…”或“请稍候…”的字样,以及加载条或 Cloudflare 标志。
不同于 Turnstile — 它是嵌入在页面内的小部件 — Cloudflare Challenge 会接管整个页面。在验证完成之前,您无法访问任何内容。
| Cloudflare Challenge | Cloudflare Turnstile | |
|---|---|---|
| 出现位置 | 全页面插页 — 完全阻止访问网站 | 嵌入页面内的小部件(例如登录表单) |
| 界面样式 | “正在验证您的浏览器…”的加载屏幕 | 表单中小勾选框或隐藏部件 |
| 由谁添加 | Cloudflare 根据安全规则自动添加 | 网站所有者嵌入在 HTML 中 |
| 解决方式 | AntiCloudflareTask — 需要代理 |
AntiTurnstileTaskProxyLess — 不需要代理 |
| 返回的 Cookie | cf_clearance(特定域名,绑定 IP) |
Turnstile 令牌(短期有效,使用一次) |
| 是否需要代理 | 是 — 解题和抓取必须使用同一 IP | 否 |
如果您看到嵌入在表单内的复选框或小部件,那就是 Turnstile — 不是此挑战。如果不确定,请查阅 CapSolver 关于识别挑战类型的指南。
这里是大多数指南跳过的问题:Cloudflare 不仅检查您的 cookie,还检查您的 TLS 指纹。
当浏览器通过 HTTPS 连接网站时,会发送包含其支持的密码套件、扩展和设置细节的 TLS ClientHello。Cloudflare 会记录这个指纹(称为 JA3 或 JA4 指纹)并与已知的浏览器配置进行比对。
Go 的 net/http、Python 的 requests、curl 以及大多数 HTTP 库都有独特的 TLS 指纹。即使 CapSolver 成功解决挑战并返回了有效的 cf_clearance cookie,如果随后抓取时检测到非浏览器的 TLS 指纹,Cloudflare 仍会重新发起挑战或阻止请求。
解决方法:使用 Go 服务器结合 httpcloak,这是一款伪装成真实 Chrome TLS 堆栈的库,包含:
这使得抓取请求在网络层级级别完全模仿 Chrome 浏览器请求。
| 要求 | 说明 |
|---|---|
| 自托管的 n8n | 必需 — TLS 服务器必须与 n8n 运行在同一台机器上。n8n Cloud 不适用此用例。 |
| CapSolver 账户 | 在这里注册 并获取 API 密钥 |
| Go 1.21 及以上版本 | 必须安装在 n8n 服务器上。用 go version 检查。 |
| 住宅或移动代理 | 数据中心代理大多数 Cloudflare 保护站点会失败。请参阅下文 代理要求。 |
CapSolver 作为 n8n 的官方集成提供 — 无需安装社区节点。
进入您的 n8n 实例,导航至 Settings → Credentials。

All您应该会看到绿色的 “Connection tested successfully” 提示条。

重要提示: 工作流中每个 CapSolver 节点都会引用此凭据。只需创建一次,所有解题工作流共享同一个凭据。
这个 Go 服务器接受来自 n8n 的抓取请求,使用 httpcloak 的 Chrome TLS 配置执行请求。它是一个小巧的独立二进制,您可以与 n8n 并行运行。
新建一个目录并保存以下代码为 main.go:
mkdir -p ~/tls-server && cd ~/tls-server
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/sardanioss/httpcloak/client"
)
type FetchRequest struct {
URL string `json:"url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
Proxy string `json:"proxy"`
Body string `json:"body"`
}
type FetchResponse struct {
Status int `json:"status"`
Body string `json:"body"`
Headers map[string][]string `json:"headers"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(ErrorResponse{Error: msg})
}
func fetchHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "only POST allowed")
return
}
var req FetchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
return
}
if req.URL == "" {
writeError(w, http.StatusBadRequest, "url is required")
return
}
if req.Method == "" {
req.Method = "GET"
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
c := client.NewClient("chrome-145", client.WithTimeout(60*time.Second))
defer c.Close()
if req.Proxy != "" {
c.SetProxy(req.Proxy)
}
// Extract user-agent separately so httpcloak uses it instead of its preset value.
headers := make(map[string][]string, len(req.Headers))
var userAgent string
for k, v := range req.Headers {
lower := strings.ToLower(k)
if lower == "user-agent" {
userAgent = v
} else {
headers[k] = []string{v}
}
}
var bodyReader io.Reader
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
}
hcReq := &client.Request{
Method: strings.ToUpper(req.Method),
URL: req.URL,
Headers: headers,
Body: bodyReader,
UserAgent: userAgent,
FetchMode: client.FetchModeNavigate,
}
resp, err := c.Do(ctx, hcReq)
if err != nil {
writeError(w, http.StatusBadGateway, "fetch failed: "+err.Error())
return
}
body, err := resp.Text()
if err != nil {
writeError(w, http.StatusInternalServerError, "read body failed: "+err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(FetchResponse{
Status: resp.StatusCode,
Body: body,
Headers: resp.Headers,
})
}
func main() {
const port = "7878"
mux := http.NewServeMux()
mux.HandleFunc("/fetch", fetchHandler)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status":"ok"}`)
})
log.Printf("TLS server (httpcloak chrome-145) listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
go mod init tls-server
go get github.com/sardanioss/httpcloak/client
go build -o main main.go
./main
curl http://localhost:7878/health
期待输出:{"status":"ok"}
注意: TLS 服务器必须与您的 n8n 实例运行在同一台机器上。n8n 工作流通过
http://localhost:7878/fetch调用它。如果您使用 n8n Cloud,则需自托管设置 — 这也是强烈推荐自托管 n8n 用于 Cloudflare Challenge 爬取的原因之一。
默认情况下,n8n 阻止 HTTP Request 节点调用 localhost 地址(防 SSRF 攻击)。您需要禁用此限制。
添加环境变量 N8N_BLOCK_ACCESS_TO_LOCALHOST=false 并重启您的 n8n 实例。具体操作取决于您的运行方式:
如果直接运行 n8n:
export N8N_BLOCK_ACCESS_TO_LOCALHOST=false
n8n start
如果使用 Docker:
在您的 docker run 命令中添加 -e N8N_BLOCK_ACCESS_TO_LOCALHOST=false,或在 docker-compose.yml 的 environment 部分添加该变量。
此工作流创建一个 POST 接口,接受受 Cloudflare 保护的 URL 和代理,借助 CapSolver 解决挑战,并返回原始的 cf_clearance cookie 和 userAgent。无需 TLS 服务器 — 由您的应用执行抓取。
Webhook (POST /solver-cloudflare-challenge) → Cloudflare Challenge (CapSolver)
→ 格式化解题结果 → 返回给 Webhook
4 个节点,纯 webhook, 无定时路径,无 TLS 服务器依赖。
websiteURL 和 proxy 的 POST 请求AntiCloudflareTask 的 CapSolver 解决挑战cookies 对象序列化为 cookie 字符串,通过 continueOnFail 处理错误cf_clearance、序列化 cookie 字符串和 userAgent### 节点配置| 设置 | 值 |
|---|---|
| HTTP 方法 | POST |
| 路径 | solver-cloudflare-challenge |
| 响应 | Response Node |
这将在以下地址创建一个端点:https://your-n8n-instance.com/webhook/solver-cloudflare-challenge
| 参数 | 值 | 说明 |
|---|---|---|
| 操作 | Cloudflare Challenge |
选择 AntiCloudflareTask |
| 类型 | AntiCloudflareTask |
全页面 Cloudflare 挑战 |
| 网站 URL | ={{ $json.body.websiteURL }} |
受 Cloudflare 保护的 URL |
| 代理 | ={{ $json.body.proxy }} |
住宅代理,格式为 host:port:user:pass |
| 失败时继续 | true |
返回结构化错误而不是崩溃 |
CapSolver 通过你的代理启动真实浏览器,加载目标 URL 并解决 Cloudflare 挑战。成功时,它返回一个包含以下内容的 solution 对象:
cookies — 一个对象 { cf_clearance: "..." } ,包含清除令牌 CookieuserAgent — 浏览器在解决过程中使用的精确 User-Agent 字符串注意: 字段为
websiteURL(而非targetURL),这与所有其他 Solver API 和 CapSolver 节点本身使用的字段名一致。
此节点是必需的,因为 AntiCloudflareTask 返回的 cookies 是一个对象({ cf_clearance: "..." }),而不是像 reCAPTCHA 或 Turnstile 那样的简单令牌字符串。它将 Cookie 序列化,提取 cf_clearance,并在 CapSolver 失败时返回结构化错误。
const input = $input.first().json;
if (input.error || !input.data || !input.data.solution) {
const errorMsg = input.error
? (input.error.message || JSON.stringify(input.error))
: 'No solution returned — site may not be showing a challenge';
return [{ json: { success: false, error: errorMsg } }];
}
const solution = input.data.solution;
const cookies = solution.cookies;
const cfClearance = (cookies && typeof cookies === 'object')
? (cookies.cf_clearance || '') : '';
const cookieString = (cookies && typeof cookies === 'object')
? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')
: (cookies || '');
return [{ json: {
success: true,
cf_clearance: cfClearance,
cookies: cookieString,
userAgent: solution.userAgent || '',
solvedAt: new Date().toISOString()
}}];
| 设置 | 值 |
|---|---|
| 响应格式 | JSON |
| 响应内容 | ={{ JSON.stringify($json) }} |
curl -X POST https://your-n8n-instance.com/webhook/solver-cloudflare-challenge \
-H "Content-Type: application/json" \
-d '{
"websiteURL": "https://protected-site.com",
"proxy": "host:port:user:pass"
}'
成功响应示例:
{
"success": true,
"cf_clearance": "abc123...",
"cookies": "cf_clearance=abc123...",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/145.0.0.0 ...",
"solvedAt": "2026-03-11T12:00:00.000Z"
}
失败响应示例(未找到挑战,代理错误等):
{
"success": false,
"error": "No solution returned — site may not be showing a challenge"
}
复制以下 JSON 并通过 n8n 的 菜单 → 从 JSON 导入 功能导入。导入后,在 Cloudflare Challenge 节点选择你的 CapSolver 认证。
{
"nodes": [
{
"parameters": {
"content": "## Cloudflare Challenge \u2014 Solver API\n\n### How it works\n\n1. Receives a request via webhook to solve a Cloudflare challenge.\n2. Uses the Cloudflare Challenge node to attempt solving the challenge.\n3. Formats the solved challenge token via custom code.\n4. Responds back to the original request with the solved token.\n\n### Setup steps\n\n- [ ] Configure the Webhook node to receive requests.\n- [ ] Set up CapSolver credentials to enable solving Cloudflare challenges.\n- [ ] Ensure the Response node is properly configured to send back the solved token.\n\n### Customization\n\nYou can customize the code in the 'Format Solution' node to change the formatting of the solved token response.",
"width": 480,
"height": 672
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-816,
-128
],
"id": "254f3829-0e6e-4ae3-bf83-85851be9a7bc",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## Receive and solve challenge\n\nTriggers on a new request and attempts to solve the Cloudflare challenge.",
"width": 512,
"height": 304,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-256,
-128
],
"id": "44e79738-a2f0-41ec-8ff0-514afbd6cc45",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## Format and return solution\n\nFormats the solution and responds back with the solved token.",
"width": 496,
"height": 304,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
352,
-128
],
"id": "fc40112d-389d-4d4e-8872-af2ae533c513",
"name": "Sticky Note2"
},
{
"parameters": {
"httpMethod": "POST",
"path": "solver-cloudflare-challenge",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-208,
0
],
"id": "cf770001-7777-7777-7777-777777777701",
"name": "Receive Solver Request",
"webhookId": "cf770001-aaaa-bbbb-cccc-777777777701",
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.body.websiteURL }}",
"proxy": "={{ $json.body.proxy }}",
"userAgent": "={{ $json.body.userAgent }}",
"html": "={{ $json.body.html }}"
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
112,
0
],
"id": "cf770001-7777-7777-7777-777777777702",
"name": "Cloudflare Challenge",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver account"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const input = $input.first().json;\n\nif (input.error || !input.data || !input.data.solution) {\n const errorMsg = input.error\n ? (input.error.message || JSON.stringify(input.error))\n : 'No solution returned \u2014 site may not be showing a challenge';\n return [{ json: { success: false, error: errorMsg } }];\n}\n\nconst solution = input.data.solution;\nconst cookies = solution.cookies;\nconst cfClearance = (cookies && typeof cookies === 'object') ? (cookies.cf_clearance || '') : '';\nconst cookieString = (cookies && typeof cookies === 'object')\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n\nreturn [{ json: {\n success: true,\n cf_clearance: cfClearance,\n cookies: cookieString,\n userAgent: solution.userAgent || '',\n solvedAt: new Date().toISOString()\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
400,
0
],
"id": "cf770001-7777-7777-7777-777777777703",
"name": "Format Solution"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
700,
0
],
"id": "cf770001-7777-7777-7777-777777777704",
"name": "Return Solved Token"
}
],
"connections": {
"Receive Solver Request": {
"main": [
[
{
"node": "Cloudflare Challenge",
"type": "main",
"index": 0
}
]
]
},
"Cloudflare Challenge": {
"main": [
[
{
"node": "Format Solution",
"type": "main",
"index": 0
}
]
]
},
"Format Solution": {
"main": [
[
{
"node": "Return Solved Token",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
到目前为止,上述 Solver API 展示了如何获取已解决的 cf_clearance Cookie 和 userAgent。但实际上你如何使用它们呢?
与需要在表单字段中提交令牌的 reCAPTCHA 或 Turnstile 不同,Cloudflare 挑战返回的是Cookie(cf_clearance),必须在后续每次请求中以请求头发送。Cookie 绑定于解题时使用的代理 IP 和 User-Agent,两者必须完全匹配。
一般流程如下:
cf_clearance Cookie 和 userAgentsec-ch-ua 头的请求头http://localhost:7878/fetch 发送请求,以匹配 Chrome 的 TLS 指纹关键点: 即使拥有有效的
cf_clearanceCookie,标准 HTTP 客户端也会失败 — 因为 Cloudflare 会指纹 TLS 握手本身。Go TLS 服务器(httpcloak)使抓取请求在网络层面完全模仿 Chrome。所有获取 Cloudflare 保护页面的工作流都必须通过 TLS 服务器。### 示例:Cloudflare 挑战抓取器

定时路径:
每 6 小时 → 设置目标配置 [定时] → 解决 Cloudflare 挑战
→ 准备 TLS 请求 → 通过 TLS 服务器获取 → 提取结果
Webhook 路径:
Webhook 触发 → 设置目标配置 [Webhook] → 解决 Cloudflare 挑战
→ 准备 TLS 请求 → 通过 TLS 服务器获取 → 提取结果 → 响应 Webhook
targetURL 和 proxy(格式为 host:port:user:pass)。定时路径使用硬编码值;Webhook 路径从 POST 请求体读取。onError: "continueRegularOutput" —— 即使页面当前没有显示挑战,也继续执行。host:port:user:pass 格式的代理转换为 http://user:pass@host:port 的 URL 格式,对 solution.cookies 进行序列化为 cookie 头字符串,并使用 solve 得到的精确 userAgent 构建类似 Chrome 的请求头。contentType: "raw"(不是 "json",因为 n8n 的 JSON 模式会损坏请求体)向 http://localhost:7878/fetch 发送 HTTP 请求。status、body 和 fetchedAt。为什么用
contentType: "raw"而不用"json"? n8n 的json内容类型模式期望请求体参数为键值对。如果你传入JSON.stringify($json)作为字符串,n8n 会把整个字符串视为单个格式错误的参数,发送{"": ""}给服务器。使用raw模式则会按表达式计算的结果精确发送请求体。
{
"nodes": [
{
"parameters": {
"content": "## Cloudflare Challenge \u2014 CapSolver + Schedule + Webhook\n\n### How it works\n\n1. Triggers schedule every 6 hours to solve Cloudflare challenges.\n2. Webhook triggers on request to solve Cloudflare challenges.\n3. Sets target and proxy configuration for requests.\n4. Solves the Cloudflare challenge using CapSolver.\n5. Sends results via TLS and processes the HTTP response.\n\n### Setup steps\n\n- Ensure CapSolver credentials are configured.\n- Confirm webhook URL is set and accessible.\n- Check schedule configuration for every 6 hours trigger.\n- Verify endpoint http://localhost:7878/fetch is available.\n\n### Customization\n\nConsider customizing the target URL or proxy settings within the set nodes.",
"width": 480,
"height": 688
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1008,
-112
],
"id": "3c1c3e96-631a-4cf6-bbff-85fd8b1a01e8",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## Scheduled Cloudflare challenge\n\nTriggered every 6 hours to solve Cloudflare challenge.",
"width": 1696,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-448,
-112
],
"id": "e9187404-7818-4233-af66-091c90a40476",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## Webhook Cloudflare challenge\n\nRespond to solver requests via webhook to solve Cloudflare challenge.",
"width": 2000,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-448,
304
],
"id": "b5d5ba23-8ed2-4afb-8113-3fa8f2fba51e",
"name": "Sticky Note2"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-400,
0
],
"id": "cf111111-1111-1111-1111-111111111101",
"name": "Every 6 Hours"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-s-001",
"name": "targetURL",
"value": "https://www.listaspam.com/busca.php?Telefono=671484239",
"type": "string"
},
{
"id": "cfg-s-002",
"name": "proxy",
"value": "YOUR_PROXY_HOST:PORT:USER:PASS",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-96,
0
],
"id": "cf111111-1111-1111-1111-111111111102",
"name": "Set Target Config [Schedule]"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.targetURL }}",
"proxy": "={{ $json.proxy }}"
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
208,
0
],
"id": "cf111111-1111-1111-1111-111111111103",
"name": "Solve Cloudflare Challenge [Schedule]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver account"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const config = $('Set Target Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n if (!proxy) return '';\n if (proxy.startsWith('http')) return proxy;\n const parts = proxy.split(':');\n if (parts.length === 4) {\n return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n }\n return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n const solution = capResult.data.solution;\n const cookies = solution.cookies;\n cookieStr = cookies && typeof cookies === 'object'\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n 'accept-language': 'en-US,en;q=0.9',\n 'sec-ch-ua': secChUa,\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'sec-fetch-dest': 'document',\n 'sec-fetch-mode': 'navigate',\n 'sec-fetch-site': 'none',\n 'sec-fetch-user': '?1',\n 'upgrade-insecure-requests': '1',\n 'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n url: config.targetURL,\n method: 'GET',\n proxy: toProxyURL(config.proxy),\n headers\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
512,
0
],
"id": "cf111111-1111-1111-1111-111111111104",
"name": "Prepare TLS Request [Schedule]"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
800,
0
],
"id": "cf111111-1111-1111-1111-111111111105",
"name": "Fetch via TLS Server [Schedule]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "out-s-001",
"name": "status",
"value": "={{ $json.status }}",
"type": "number"
},
{
"id": "out-s-002",
"name": "body",
"value": "={{ $json.body }}",
"type": "string"
},
{
"id": "out-s-003",
"name": "fetchedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1104,
0
],
"id": "cf111111-1111-1111-1111-111111111106",
"name": "Extract Result [Schedule]"
},
{
"parameters": {
"httpMethod": "POST",
"path": "cloudflare-scraper",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-400,
420
],
"id": "cf111111-1111-1111-1111-111111111107",
"name": "Receive Solver Request",
"webhookId": "cf111111-aaaa-bbbb-cccc-111111111107",
"onError": "continueRegularOutput"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-w-001",
"name": "targetURL",
"value": "={{ $json.body.targetURL }}",
"type": "string"
},
{
"id": "cfg-w-002",
"name": "proxy",
"value": "={{ $json.body.proxy }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-96,
420
],
"id": "cf111111-1111-1111-1111-111111111108",
"name": "Set Target Config [Webhook]"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.targetURL }}",
"proxy": "={{ $json.proxy }}"
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
208,
420
],
"id": "cf111111-1111-1111-1111-111111111109",
"name": "Solve Cloudflare Challenge [Webhook]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver account"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const config = $('Set Target Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n if (!proxy) return '';\n if (proxy.startsWith('http')) return proxy;\n const parts = proxy.split(':');\n if (parts.length === 4) {\n return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n }\n return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n const solution = capResult.data.solution;\n const cookies = solution.cookies;\n cookieStr = cookies && typeof cookies === 'object'\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n 'accept-language': 'en-US,en;q=0.9',\n 'sec-ch-ua': secChUa,\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'sec-fetch-dest': 'document',\n 'sec-fetch-mode': 'navigate',\n 'sec-fetch-site': 'none',\n 'sec-fetch-user': '?1',\n 'upgrade-insecure-requests': '1',\n 'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n url: config.targetURL,\n method: 'GET',\n proxy: toProxyURL(config.proxy),\n headers\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
512,
420
],
"id": "cf111111-1111-1111-1111-111111111110",
"name": "Prepare TLS Request [Webhook]"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
800,
420
],
"id": "cf111111-1111-1111-1111-111111111111",
"name": "Fetch via TLS Server [Webhook]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "out-w-001",
"name": "status",
"value": "={{ $json.status }}",
"type": "number"
},
{
"id": "out-w-002",
"name": "body",
"value": "={{ $json.body }}",
"type": "string"
},
{
"id": "out-w-003",
"name": "fetchedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1104,
420
],
"id": "cf111111-1111-1111-1111-111111111112",
"name": "Extract Result [Webhook]"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1408,
420
],
"id": "cf111111-1111-1111-1111-111111111113",
"name": "Return Solved Token"
}
],
"connections": {
"Every 6 Hours": {
"main": [
[
{
"node": "Set Target Config [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Set Target Config [Schedule]": {
"main": [
[
{
"node": "Solve Cloudflare Challenge [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Solve Cloudflare Challenge [Schedule]": {
"main": [
[
{
"node": "Prepare TLS Request [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Prepare TLS Request [Schedule]": {
"main": [
[
{
"node": "Fetch via TLS Server [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Fetch via TLS Server [Schedule]": {
"main": [
[
{
"node": "Extract Result [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Receive Solver Request": {
"main": [
[
{
"node": "Set Target Config [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Set Target Config [Webhook]": {
"main": [
[
{
"node": "Solve Cloudflare Challenge [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Solve Cloudflare Challenge [Webhook]": {
"main": [
[
{
"node": "Prepare TLS Request [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Prepare TLS Request [Webhook]": {
"main": [
[
{
"node": "Fetch via TLS Server [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Fetch via TLS Server [Webhook]": {
"main": [
[
{
"node": "Extract Result [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Extract Result [Webhook]": {
"main": [
[
{
"node": "Return Solved Token",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
---## 工作流程:用例示例
上面的 Solver API 和爬虫示例展示了核心模式:解决验证码挑战,然后使用解决方案通过 TLS 进行抓取。以下工作流程将此模式扩展到生产就绪的用例——每个用例都具有双重触发器(定时 + webhook)、持久状态跟踪和结构化输出。每个用例都需要相同的先决条件:自托管的 n8n 实例、运行在 7878 端口的 TLS 服务器、住宅代理和 CapSolver 凭证。
| 工作流程 | 目的 |
|---|---|
Cloudflare Challenge Scraping — 价格与产品详情 — CapSolver + 定时 + Webhook |
每 6 小时从 CF 保护页面爬取价格和产品名称,与之前的值比较,发生变化时发送警报 |
Cloudflare Challenge 账户登录 — CapSolver + 定时 + Webhook |
先解决挑战验证码,再通过 TLS 服务器 POST 凭证,以登录你自己在 CF 保护网站上的账户 |
Turnstile — Solver API |
提供一个 webhook 来解决 Turnstile 并返回令牌——无需代理或 TLS 服务器 |
Turnstile 爬虫 — 价格与产品监控 |
解决 Turnstile,使用令牌抓取产品页面,提取价格和名称,发生变化时发送警报 |
Turnstile 账户登录 |
先解决挑战验证码,然后使用令牌 POST 凭证登录你在 Turnstile 保护网站上的账户——无需代理或 TLS 服务器 |
此工作流每 6 小时(计划任务)或按需(Webhook)抓取一次产品页面,使用 HTML 节点提取价格,并与之前存储的值进行比较。
计划路径:
每 6 小时 → 设置目标配置 → 解决 Cloudflare 挑战 → 准备 TLS 请求
→ 通过 TLS 服务器抓取 → 提取数据 → 比较数据
→ 数据是否变更? → 构建警报 / 无变化
错误处理: 如果 CapSolver 失败,工作流继续运行但不使用 cookies(通过
continueOnFail)。如果页面当前没有显示挑战,TLS 服务器抓取仍可能成功。
关键行为:
dataPropertyName: "body"(而非 "data"),因为 TLS 服务器返回 { status, body, headers }.product-price,h1)提取价格和产品名称$workflow.staticData.lastPrice 用于在执行间持久保存上次价格deal)和上涨(严重级别:info)host:port:user:pass → http://user:pass@host:port,通过 toProxyURL() 辅助函数{
"nodes": [
{
"parameters": {
"content": "## Cloudflare 挑战抓取 — 价格与产品详情 — CapSolver + 计划任务 + Webhook\n\n### 工作原理\n\n1. 触发器被设置为定期检查目标网站,或响应外部 webhook。\n2. 应用目标 URL 和代理配置。\n3. 解决 Cloudflare 挑战以绕过网站防护。\n4. 发起安全请求抓取目标服务器数据。\n5. 提取的数据进行比较以检测变化。\n6. 根据检测结果构建并发送警报或返回数据。\n\n### 设置步骤\n\n- [ ] 在 '设置目标配置' 节点中配置 URL 和代理设置。\n- [ ] 连接 CapSolver 凭据,用于解决 Cloudflare 挑战。\n- [ ] 确保 webhook URL 配置正确以接收外部请求。\n\n### 自定义\n\n调整监控频率,修改 '每 6 小时' 节点的时间间隔或 webhook 触发器设置。",
"width": 480,
"height": 896
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1104,
-192
],
"id": "85c55c3d-335a-47e5-8721-82fa4d633033",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 计划触发器设置\n\n每 6 小时初始化调度以开始数据抓取流程。",
"width": 1392,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-544,
-112
],
"id": "6ac3bd35-66ef-43ed-a8b5-5d5f6d367fba",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 计划数据处理\n\n在计划触发后处理数据提取、比较和警报构建。",
"width": 1088,
"height": 480,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
960,
-192
],
"id": "a1c60513-ed8b-43f2-bff3-a7b19436337f",
"name": "Sticky Note2"
},
{
"parameters": {
"content": "## Webhook 触发器设置\n\n通过 webhook 监控外部请求以开始抓取流程。",
"width": 1392,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-544,
384
],
"id": "ee4d9a0d-3233-4b70-828b-078c9eee0086",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## Webhook 数据处理与响应\n\n从 webhook 触发器提取并比较数据;根据变化返回响应。",
"width": 1392,
"height": 480,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
960,
336
],
"id": "4ad55fbf-2882-4c63-9e09-37e674b00145",
"name": "Sticky Note4"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-500,
0
],
"id": "cf333333-3333-3333-3333-333333333301",
"name": "每 6 小时"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-s-001",
"name": "targetURL",
"value": "https://YOUR_CF_PROTECTED_SITE.com/product-page",
"type": "string"
},
{
"id": "cfg-s-002",
"name": "proxy",
"value": "YOUR_PROXY_HOST:PORT:USER:PASS",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-200,
0
],
"id": "cf333333-3333-3333-3333-333333333302",
"name": "设置目标配置 [计划]"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.targetURL }}",
"proxy": "={{ $json.proxy }}"
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
100,
0
],
"id": "cf333333-3333-3333-3333-333333333303",
"name": "解决 Cloudflare 挑战 [计划]",
"onError": "continueRegularOutput",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver 账户"
}
}
},
{
"parameters": {
"jsCode": "const config = $('Set Target Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n if (!proxy) return '';\n if (proxy.startsWith('http')) return proxy;\n const parts = proxy.split(':');\n if (parts.length === 4) {\n return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n }\n return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n const solution = capResult.data.solution;\n const cookies = solution.cookies;\n cookieStr = cookies && typeof cookies === 'object'\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n 'accept-language': 'en-US,en;q=0.9',\n 'sec-ch-ua': secChUa,\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'sec-fetch-dest': 'document',\n 'sec-fetch-mode': 'navigate',\n 'sec-fetch-site': 'none',\n 'sec-fetch-user': '?1',\n 'upgrade-insecure-requests': '1',\n 'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n url: config.targetURL,\n method: 'GET',\n proxy: toProxyURL(config.proxy),\n headers\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
400,
0
],
"id": "cf333333-3333-3333-3333-333333333304",
"name": "准备 TLS 请求 [计划]"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
700,
0
],
"id": "cf333333-3333-3333-3333-333333333305",
"name": "通过 TLS 服务器抓取 [计划]"
},
{
"parameters": {
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "price",
"cssSelector": ".product-price, [data-price], .price",
"returnValue": "text",
"returnArray": false
},
{
"key": "productName",
"cssSelector": "h1, .product-title",
"returnValue": "text",
"returnArray": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
1000,
0
],
"id": "cf333333-3333-3333-3333-333333333306",
"name": "提取数据"
},
{
"parameters": {
"jsCode": "const staticData = $workflow.staticData;\nconst currentPrice = $input.first().json.price;\nconst previousPrice = staticData.lastPrice;\nconst productName = $input.first().json.productName || '产品';\n\nconst parsePrice = (str) => {\n if (!str) return null;\n const match = str.match(/[\\d]+\\.?\\d*/);\n return match ? parseFloat(match[0].replace(',', '')) : null;\n};\n\nconst currentNum = parsePrice(currentPrice);\nconst previousNum = parsePrice(previousPrice);\n\nstaticData.lastPrice = currentPrice;\nstaticData.lastChecked = new Date().toISOString();\n\nconst changed = previousNum !== null && currentNum !== null && currentNum !== previousNum;\nconst direction = changed ? (currentNum < previousNum ? '下降' : '上涨') : '无变化';\nconst diff = changed ? Math.abs(currentNum - previousNum).toFixed(2) : '0';\n\nreturn [{\n json: {\n productName,\n currentPrice,\n previousPrice: previousPrice || '首次检查',\n changed,\n direction,\n diff: changed ? `¥${diff}` : null,\n checkedAt: new Date().toISOString()\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1300,
0
],
"id": "cf333333-3333-3333-3333-333333333307",
"name": "比较数据"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "price-if-001",
"leftValue": "={{ $json.changed }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1600,
0
],
"id": "cf333333-3333-3333-3333-333333333308",
"name": "数据是否变更?"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "alert-001",
"name": "alert",
"value": "=价格{{ $json.direction }}:{{ $json.productName }},从 {{ $json.previousPrice }} 变为 {{ $json.currentPrice }} ({{ $json.direction === '下降' ? '-' : '+' }}{{ $json.diff }})",
"type": "string"
},
{
"id": "alert-002",
"name": "severity",
"value": "={{ $json.direction === '下降' ? 'deal' : 'info' }}",
"type": "string"
},
{
"id": "alert-003",
"name": "checkedAt",
"value": "={{ $json.checkedAt }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1900,
-80
],
"id": "cf333333-3333-3333-3333-333333333309",
"name": "构建警报"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "nc-001",
"name": "status",
"value": "no_change",
"type": "string"
},
{
"id": "nc-002",
"name": "currentPrice",
"value": "={{ $json.currentPrice }}",
"type": "string"
},
{
"id": "nc-003",
"name": "checkedAt",
"value": "={{ $json.checkedAt }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1900,
128
],
"id": "cf333333-3333-3333-3333-333333333310",
"name": "无变化"
},
{
"parameters": {
"httpMethod": "POST",
"path": "cloudflare-price-monitor",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-500,
500
],
"id": "cf333333-3333-3333-3333-333333333311",
"name": "接收监控请求",
"webhookId": "cf333333-aaaa-bbbb-cccc-333333333311",
"onError": "continueRegularOutput"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-w-001",
"name": "targetURL",
"value": "={{ $json.body.targetURL }}",
"type": "string"
},
{
"id": "cfg-w-002",
"name": "proxy",
"value": "={{ $json.body.proxy }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-200,
500
],
"id": "cf333333-3333-3333-3333-333333333312",
"name": "设置目标配置 [Webhook]"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.targetURL }}",
"proxy": "={{ $json.proxy }}"
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
100,
500
],
"id": "cf333333-3333-3333-3333-333333333313",
"name": "解决 Cloudflare 挑战 [Webhook]",
"onError": "continueRegularOutput",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver 账户"
}
}
},
{
"parameters": {
"jsCode": "const config = $('Set Target Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n if (!proxy) return '';\n if (proxy.startsWith('http')) return proxy;\n const parts = proxy.split(':');\n if (parts.length === 4) {\n return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n }\n return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n const solution = capResult.data.solution;\n const cookies = solution.cookies;\n cookieStr = cookies && typeof cookies === 'object'\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n 'accept-language': 'en-US,en;q=0.9',\n 'sec-ch-ua': secChUa,\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'sec-fetch-dest': 'document',\n 'sec-fetch-mode': 'navigate',\n 'sec-fetch-site': 'none',\n 'sec-fetch-user': '?1',\n 'upgrade-insecure-requests': '1',\n 'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n url: config.targetURL,\n method: 'GET',\n proxy: toProxyURL(config.proxy),\n headers\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
400,
500
],
"id": "cf333333-3333-3333-3333-333333333314",
"name": "准备 TLS 请求 [Webhook]"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
700,
500
],
"id": "cf333333-3333-3333-3333-333333333315",
"name": "通过 TLS 服务器抓取 [Webhook]"
},
{
"parameters": {
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "price",
"cssSelector": ".product-price, [data-price], .price",
"returnValue": "text",
"returnArray": false
},
{
"key": "productName",
"cssSelector": "h1, .product-title",
"returnValue": "text",
"returnArray": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
1008,
528
],
"id": "cf333333-3333-3333-3333-333333333316",
"name": "提取数据 [Webhook]"
},
{
"parameters": {
"jsCode": "const staticData = $workflow.staticData;\nconst currentPrice = $input.first().json.price;\nconst previousPrice = staticData.lastPrice;\nconst productName = $input.first().json.productName || '产品';\n\nconst parsePrice = (str) => {\n if (!str) return null;\n const match = str.match(/[\\d]+\\.?\\d*/);\n return match ? parseFloat(match[0].replace(',', '')) : null;\n};\n\nconst currentNum = parsePrice(currentPrice);\nconst previousNum = parsePrice(previousPrice);\n\nstaticData.lastPrice = currentPrice;\nstaticData.lastChecked = new Date().toISOString();\n\nconst changed = previousNum !== null && currentNum !== null && currentNum !== previousNum;\nconst direction = changed ? (currentNum < previousNum ? '下降' : '上涨') : '无变化';\nconst diff = changed ? Math.abs(currentNum - previousNum).toFixed(2) : '0';\n\nreturn [{\n json: {\n productName,\n currentPrice,\n previousPrice: previousPrice || '首次检查',\n changed,\n direction,\n diff: changed ? `¥${diff}` : null,\n checkedAt: new Date().toISOString()\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1296,
528
],
"id": "cf333333-3333-3333-3333-333333333317",
"name": "比较数据 [Webhook]"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "price-if-002",
"leftValue": "={{ $json.changed }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1600,
528
],
"id": "cf333333-3333-3333-3333-333333333318",
"name": "数据是否变更? [Webhook]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "alert-004",
"name": "alert",
"value": "=价格{{ $json.direction }}:{{ $json.productName }},从 {{ $json.previousPrice }} 变为 {{ $json.currentPrice }} ({{ $json.direction === '下降' ? '-' : '+' }}{{ $json.diff }})",
"type": "string"
},
{
"id": "alert-005",
"name": "severity",
"value": "={{ $json.direction === '下降' ? 'deal' : 'info' }}",
"type": "string"
},
{
"id": "alert-006",
"name": "checkedAt",
"value": "={{ $json.checkedAt }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1904,
448
],
"id": "cf333333-3333-3333-3333-333333333319",
"name": "构建警报 [Webhook]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "nc-004",
"name": "status",
"value": "no_change",
"type": "string"
},
{
"id": "nc-005",
"name": "currentPrice",
"value": "={{ $json.currentPrice }}",
"type": "string"
},
{
"id": "nc-006",
"name": "checkedAt",
"value": "={{ $json.checkedAt }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1904,
656
],
"id": "cf333333-3333-3333-3333-333333333320",
"name": "无变化 [Webhook]"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
2208,
528
],
"id": "cf333333-3333-3333-3333-333333333321",
"name": "返回抓取数据"
}
],
"connections": {
"每 6 小时": {
"main": [
[
{
"node": "设置目标配置 [计划]",
"type": "main",
"index": 0
}
]
]
},
"设置目标配置 [计划]": {
"main": [
[
{
"node": "解决 Cloudflare 挑战 [计划]",
"type": "main",
"index": 0
}
]
]
},
"解决 Cloudflare 挑战 [计划]": {
"main": [
[
{
"node": "准备 TLS 请求 [计划]",
"type": "main",
"index": 0
}
]
]
},
"准备 TLS 请求 [计划]": {
"main": [
[
{
"node": "通过 TLS 服务器抓取 [计划]",
"type": "main",
"index": 0
}
]
]
},
"通过 TLS 服务器抓取 [计划]": {
"main": [
[
{
"node": "提取数据",
"type": "main",
"index": 0
}
]
]
},
"提取数据": {
"main": [
[
{
"node": "比较数据",
"type": "main",
"index": 0
}
]
]
},
"比较数据": {
"main": [
[
{
"node": "数据是否变更?",
"type": "main",
"index": 0
}
]
]
},
"数据是否变更?": {
"main": [
[
{
"node": "构建警报",
"type": "main",
"index": 0
}
],
[
{
"node": "无变化",
"type": "main",
"index": 0
}
]
]
},
"接收监控请求": {
"main": [
[
{
"node": "设置目标配置 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"设置目标配置 [Webhook]": {
"main": [
[
{
"node": "解决 Cloudflare 挑战 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"解决 Cloudflare 挑战 [Webhook]": {
"main": [
[
{
"node": "准备 TLS 请求 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"准备 TLS 请求 [Webhook]": {
"main": [
[
{
"node": "通过 TLS 服务器抓取 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"通过 TLS 服务器抓取 [Webhook]": {
"main": [
[
{
"node": "提取数据 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"提取数据 [Webhook]": {
"main": [
[
{
"node": "比较数据 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"比较数据 [Webhook]": {
"main": [
[
{
"node": "数据是否变更? [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"数据是否变更? [Webhook]": {
"main": [
[
{
"node": "构建警报 [Webhook]",
"type": "main",
"index": 0
}
],
[
{
"node": "无变化 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"构建警报 [Webhook]": {
"main": [
[
{
"node": "返回抓取数据",
"type": "main",
"index": 0
}
]
]
},
"无变化 [Webhook]": {
"main": [
[
{
"node": "返回抓取数据",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
此工作流自动化登录到受 Cloudflare 保护的网站。Set Login Config 节点集中管理所有参数 — [Schedule] 用于定时路径,[Webhook] 用于按需 webhook 路径。
定时路径:
每 24 小时 → Set Login Config → 解决 CF 挑战
→ 准备 TLS 登录请求 → 通过 TLS 服务器提交登录
→ 登录成功? → 标记成功 / 标记失败
错误处理: 如果 CapSolver 失败,工作流会通过
continueOnFail继续执行但不带 cookies。登录请求很可能失败,登录成功?节点会捕获此情况。
关键行为:
cf_clearance cookie + userAgent 作为 HTTP 头部(不会在表单体中提交 token — 不同于提交 g-recaptcha-response 的 reCAPTCHA 登录)URLSearchParams 设置 — 在 Set Login Config 中编辑字段名(usernameField、passwordField)以匹配你的站点status < 400 且响应体中包含 successMarker{
"nodes": [
{
"parameters": {
"content": "## Cloudflare 挑战账户登录 \u2014 CapSolver + 定时 + Webhook\n\n### 工作原理\n\n1. 每 24 小时调度一次登录流程并解决 Cloudflare 挑战。\n2. 当由定时触发时,准备并提交 TLS 登录请求。\n3. 处理定时登录的成功或失败并记录结果。\n4. 通过 webhook 接收登录请求并解决 Cloudflare 挑战。\n5. 当由 webhook 触发时,准备并提交 TLS 登录请求。\n6. 处理 webhook 登录的成功或失败并返回结果。\n\n### 设置步骤\n\n- [ ] 确保配置了 Cloudflare CapSolver API 凭据。\n- [ ] 搭建本地运行的 TLS 服务器,地址 http://localhost:7878。\n- [ ] 配置 webhook URL 以接收登录请求。\n\n### 自定义\n\n调整定时间隔以满足具体频率需求。",
"width": 480,
"height": 896
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1248,
-320
],
"id": "ba1d6098-8cd2-40f1-b9ae-b945303e5d12",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 定时登录开始\n\n每 24 小时开始登录流程作为后续操作的触发点。",
"width": 240,
"height": 336,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-688,
-256
],
"id": "acc8f2a2-298f-4d1b-9db7-8a98fe626abb",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 定时登录工作流\n\n处理 Cloudflare 挑战,准备并提交登录请求,每 24 小时检查登录是否成功。",
"width": 1328,
"height": 480,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-416,
-320
],
"id": "1259d9f4-0b54-4693-89af-193f3ccda6a0",
"name": "Sticky Note2"
},
{
"parameters": {
"content": "## Webhook 登录开始\n\n通过 webhook 接收登录请求以启动后续流程。",
"width": 240,
"height": 320,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-688,
272
],
"id": "e95374d2-4029-469d-8e8f-a9afb66ae2ed",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## Webhook 登录工作流\n\n处理接收到的 webhook 登录请求,解决 Cloudflare 挑战,准备并提交登录请求,检查是否成功,记录结果。",
"width": 1216,
"height": 528,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-416,
192
],
"id": "da56a87e-6675-439a-8c65-7da631a86df1",
"name": "Sticky Note4"
},
{
"parameters": {
"content": "## 返回 webhook 结果\n\n以登录尝试结果响应最初的 webhook 请求。",
"width": 240,
"height": 320,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
832,
256
],
"id": "7a3f246c-ebd5-419f-a955-b01669743b31",
"name": "Sticky Note5"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-640,
-80
],
"id": "cf666666-6666-6666-6666-666666666601",
"name": "每 24 小时"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "login-001",
"name": "targetURL",
"value": "https://YOUR_CF_PROTECTED_SITE.com/login",
"type": "string"
},
{
"id": "login-002",
"name": "loginActionURL",
"value": "https://YOUR_CF_PROTECTED_SITE.com/login",
"type": "string"
},
{
"id": "login-003",
"name": "proxy",
"value": "YOUR_PROXY_HOST:PORT:USER:PASS",
"type": "string"
},
{
"id": "login-004",
"name": "usernameField",
"value": "email",
"type": "string"
},
{
"id": "login-005",
"name": "passwordField",
"value": "password",
"type": "string"
},
{
"id": "login-006",
"name": "usernameValue",
"value": "your-email@example.com",
"type": "string"
},
{
"id": "login-007",
"name": "passwordValue",
"value": "YOUR_ACCOUNT_PASSWORD",
"type": "string"
},
{
"id": "login-008",
"name": "successMarker",
"value": "account-dashboard",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-368,
-80
],
"id": "cf666666-6666-6666-6666-666666666602",
"name": "设置登录配置 [定时]"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.targetURL }}",
"proxy": "={{ $json.proxy }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
-144,
-64
],
"id": "cf666666-6666-6666-6666-666666666603",
"name": "解决 Cloudflare 挑战 [定时]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver 账户"
}
}
},
{
"parameters": {
"jsCode": "const config = $('Set Login Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n if (!proxy) return '';\n if (proxy.startsWith('http')) return proxy;\n const parts = proxy.split(':');\n if (parts.length === 4) {\n return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n }\n return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n const solution = capResult.data.solution;\n const cookies = solution.cookies;\n cookieStr = cookies && typeof cookies === 'object'\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst params = new URLSearchParams();\nparams.set(config.usernameField, config.usernameValue);\nparams.set(config.passwordField, config.passwordValue);\n\nconst headers = {\n 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n 'accept-language': 'en-US,en;q=0.9',\n 'content-type': 'application/x-www-form-urlencoded',\n 'sec-ch-ua': secChUa,\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'sec-fetch-dest': 'document',\n 'sec-fetch-mode': 'navigate',\n 'sec-fetch-site': 'same-origin',\n 'sec-fetch-user': '?1',\n 'upgrade-insecure-requests': '1',\n 'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n url: config.loginActionURL,\n method: 'POST',\n proxy: toProxyURL(config.proxy),\n headers,\n body: params.toString()\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
48,
-64
],
"id": "cf666666-6666-6666-6666-666666666604",
"name": "准备 TLS 登录请求 [定时]"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
240,
-64
],
"id": "cf666666-6666-6666-6666-666666666605",
"name": "通过 TLS 服务器提交登录 [定时]"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "login-if-001",
"leftValue": "={{ $json.status < 400 && String($json.body || '').includes($('Set Login Config [Schedule]').item.json.successMarker) }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
432,
-64
],
"id": "cf666666-6666-6666-6666-666666666606",
"name": "登录成功? [定时]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "login-010",
"name": "action",
"value": "account_login",
"type": "string"
},
{
"id": "login-011",
"name": "status",
"value": "success",
"type": "string"
},
{
"id": "login-012",
"name": "message",
"value": "配置的账户登录流程成功(通过 Cloudflare Challenge 绕过)",
"type": "string"
},
{
"id": "login-013",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
768,
-208
],
"id": "cf666666-6666-6666-6666-666666666607",
"name": "标记登录成功 [定时]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "login-014",
"name": "action",
"value": "account_login",
"type": "string"
},
{
"id": "login-015",
"name": "status",
"value": "failed",
"type": "string"
},
{
"id": "login-016",
"name": "statusCode",
"value": "={{ $json.status }}",
"type": "number"
},
{
"id": "login-017",
"name": "message",
"value": "登录响应未匹配配置的成功标志",
"type": "string"
},
{
"id": "login-018",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
768,
0
],
"id": "cf666666-6666-6666-6666-666666666608",
"name": "标记登录失败 [定时]"
},
{
"parameters": {
"httpMethod": "POST",
"path": "cloudflare-account-login",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-640,
432
],
"id": "cf666666-6666-6666-6666-666666666609",
"name": "接收登录请求",
"webhookId": "cf666666-aaaa-bbbb-cccc-666666666609"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "login-019",
"name": "targetURL",
"value": "={{ $json.body.targetURL }}",
"type": "string"
},
{
"id": "login-020",
"name": "loginActionURL",
"value": "={{ $json.body.loginActionURL }}",
"type": "string"
},
{
"id": "login-021",
"name": "proxy",
"value": "={{ $json.body.proxy }}",
"type": "string"
},
{
"id": "login-022",
"name": "usernameField",
"value": "={{ $json.body.usernameField }}",
"type": "string"
},
{
"id": "login-023",
"name": "passwordField",
"value": "={{ $json.body.passwordField }}",
"type": "string"
},
{
"id": "login-024",
"name": "usernameValue",
"value": "={{ $json.body.usernameValue }}",
"type": "string"
},
{
"id": "login-025",
"name": "passwordValue",
"value": "={{ $json.body.passwordValue }}",
"type": "string"
},
{
"id": "login-026",
"name": "successMarker",
"value": "={{ $json.body.successMarker }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-368,
480
],
"id": "cf666666-6666-6666-6666-666666666610",
"name": "设置登录配置 [Webhook]"
},
{
"parameters": {
"operation": "Cloudflare Challenge",
"websiteURL": "={{ $json.targetURL }}",
"proxy": "={{ $json.proxy }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
-144,
480
],
"id": "cf666666-6666-6666-6666-666666666611",
"name": "解决 Cloudflare 挑战 [Webhook]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver 账户"
}
}
},
{
"parameters": {
"jsCode": "const config = $('Set Login Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n if (!proxy) return '';\n if (proxy.startsWith('http')) return proxy;\n const parts = proxy.split(':');\n if (parts.length === 4) {\n return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n }\n return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n const solution = capResult.data.solution;\n const cookies = solution.cookies;\n cookieStr = cookies && typeof cookies === 'object'\n ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n : (cookies || '');\n if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst params = new URLSearchParams();\nparams.set(config.usernameField, config.usernameValue);\nparams.set(config.passwordField, config.passwordValue);\n\nconst headers = {\n 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n 'accept-language': 'en-US,en;q=0.9',\n 'content-type': 'application/x-www-form-urlencoded',\n 'sec-ch-ua': secChUa,\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'sec-fetch-dest': 'document',\n 'sec-fetch-mode': 'navigate',\n 'sec-fetch-site': 'same-origin',\n 'sec-fetch-user': '?1',\n 'upgrade-insecure-requests': '1',\n 'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n url: config.loginActionURL,\n method: 'POST',\n proxy: toProxyURL(config.proxy),\n headers,\n body: params.toString()\n}}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
48,
480
],
"id": "cf666666-6666-6666-6666-666666666612",
"name": "准备 TLS 登录请求 [Webhook]"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
256,
480
],
"id": "cf666666-6666-6666-6666-666666666613",
"name": "通过 TLS 服务器提交登录 [Webhook]"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "login-if-002",
"leftValue": "={{ $json.status < 400 && String($json.body || '').includes($('Set Login Config [Webhook]').item.json.successMarker) }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
448,
496
],
"id": "cf666666-6666-6666-6666-666666666614",
"name": "登录成功? [Webhook]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "login-028",
"name": "action",
"value": "account_login",
"type": "string"
},
{
"id": "login-029",
"name": "status",
"value": "success",
"type": "string"
},
{
"id": "login-030",
"name": "message",
"value": "配置的账户登录流程成功(通过 Cloudflare Challenge 绕过)",
"type": "string"
},
{
"id": "login-031",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
640,
320
],
"id": "cf666666-6666-6666-6666-666666666615",
"name": "标记登录成功 [Webhook]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "login-032",
"name": "action",
"value": "account_login",
"type": "string"
},
{
"id": "login-033",
"name": "status",
"value": "failed",
"type": "string"
},
{
"id": "login-034",
"name": "statusCode",
"value": "={{ $json.status }}",
"type": "number"
},
{
"id": "login-035",
"name": "message",
"value": "登录响应未匹配配置的成功标志",
"type": "string"
},
{
"id": "login-036",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
656,
544
],
"id": "cf666666-6666-6666-6666-666666666616",
"name": "标记登录失败 [Webhook]"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
880,
416
],
"id": "cf666666-6666-6666-6666-666666666617",
"name": "返回登录结果"
}
],
"connections": {
"每 24 小时": {
"main": [
[
{
"node": "设置登录配置 [定时]",
"type": "main",
"index": 0
}
]
]
},
"设置登录配置 [定时]": {
"main": [
[
{
"node": "解决 Cloudflare 挑战 [定时]",
"type": "main",
"index": 0
}
]
]
},
"解决 Cloudflare 挑战 [定时]": {
"main": [
[
{
"node": "准备 TLS 登录请求 [定时]",
"type": "main",
"index": 0
}
]
]
},
"准备 TLS 登录请求 [定时]": {
"main": [
[
{
"node": "通过 TLS 服务器提交登录 [定时]",
"type": "main",
"index": 0
}
]
]
},
"通过 TLS 服务器提交登录 [定时]": {
"main": [
[
{
"node": "登录成功? [定时]",
"type": "main",
"index": 0
}
]
]
},
"登录成功? [定时]": {
"main": [
[
{
"node": "标记登录成功 [定时]",
"type": "main",
"index": 0
}
],
[
{
"node": "标记登录失败 [定时]",
"type": "main",
"index": 0
}
]
]
},
"接收登录请求": {
"main": [
[
{
"node": "设置登录配置 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"设置登录配置 [Webhook]": {
"main": [
[
{
"node": "解决 Cloudflare 挑战 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"解决 Cloudflare 挑战 [Webhook]": {
"main": [
[
{
"node": "准备 TLS 登录请求 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"准备 TLS 登录请求 [Webhook]": {
"main": [
[
{
"node": "通过 TLS 服务器提交登录 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"通过 TLS 服务器提交登录 [Webhook]": {
"main": [
[
{
"node": "登录成功? [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"登录成功? [Webhook]": {
"main": [
[
{
"node": "标记登录成功 [Webhook]",
"type": "main",
"index": 0
}
],
[
{
"node": "标记登录失败 [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"标记登录成功 [Webhook]": {
"main": [
[
{
"node": "返回登录结果",
"type": "main",
"index": 0
}
]
]
},
"标记登录失败 [Webhook]": {
"main": [
[
{
"node": "返回登录结果",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
此工作流创建了一个 POST 端点,用于解决 Cloudflare Turnstile 验证挑战并返回令牌。它是上面Cloudflare Challenge Solver API的 Turnstile 等价物——但更简单:无代理、无 TLS 服务器,仅需 3 个节点。
流程:
Webhook (POST /solver-turnstile) → 解决 Turnstile (CapSolver) → 响应 Webhook
websiteURL 和 websiteKey 的 POST 请求AntiTurnstileTaskProxyLess 解决 Turnstile 挑战token)与 Cloudflare Challenge 的主要区别: Turnstile 返回的是一个
token字符串,而不是cf_clearancecookie。你会在后续请求中以cf-turnstile-response请求头(或表单字段,取决于网站)发送此令牌。无需代理。
curl -X POST https://your-n8n-instance.com/webhook/solver-turnstile \
-H "Content-Type: application/json" \
-d '{
"websiteURL": "https://target-site.com/page",
"websiteKey": "0x4AAAAAAA..."
}'
复制下方 JSON,然后通过 n8n 中的 菜单 → 从 JSON 导入 导入。导入后,在“解决 Turnstile”节点中选择你的 CapSolver 凭据。
{
"nodes": [
{
"parameters": {
"content": "## Turnstile \u2014 Solver API\n\n### How it works\n\n1. Receives a solver request through a webhook.\n2. Solves the Turnstile CAPTCHA using a specialized solver node.\n3. Sends the response back via a webhook response.\n\n### Setup steps\n\n- [ ] Configure webhook URL for receiving requests.\n- [ ] Set up credentials for the capSolver node.\n- [ ] Ensure response webhook URL is correctly set up.\n\n### Customization\n\nThe solver node configuration can be adjusted to handle different types of Turnstile CAPTCHAs.",
"width": 480,
"height": 560
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-848,
-80
],
"id": "d52f67cb-cb00-430f-bd76-b74cf4fe6184",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## Handle solver request\n\nReceives and processes a request to solve a Turnstile CAPTCHA, then sends the result back.",
"width": 832,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-288,
-80
],
"id": "21f62617-eaa2-41ae-8fb8-c8502f21c275",
"name": "Sticky Note1"
},
{
"parameters": {
"httpMethod": "POST",
"path": "solver-turnstile",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-240,
32
],
"id": "ts-001",
"name": "Receive Solver Request",
"webhookId": "a7ef0297-8455-44bd-9305-26c179f040b5",
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "Cloudflare Turnstile",
"websiteURL": "={{ $json.body.websiteURL }}",
"websiteKey": "={{ $json.body.websiteKey }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
80,
32
],
"id": "ts-002",
"name": "Solve Turnstile",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver account"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json.data) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
400,
32
],
"id": "ts-003",
"name": "Respond to Webhook"
}
],
"connections": {
"Receive Solver Request": {
"main": [
[
{
"node": "Solve Turnstile",
"type": "main",
"index": 0
}
]
]
},
"Solve Turnstile": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
此工作流程解决 Cloudflare Turnstile 验证,使用解决后的 token 获取产品页面,提取价格和产品名称,并与之前的值进行比较 —— 在有变动时发出警报。它遵循与上文 Cloudflare Challenge 抓取示例相同的双触发模式(定时 + webhook)。
与 Cloudflare Challenge 抓取的主要区别: 无需 TLS 服务器,无需代理,无需
Prepare TLS Request代码节点。Turnstile token 作为cf-turnstile-response头直接通过 n8n 内置的 HTTP 请求节点发送。
定时路径:
每 6 小时 → 设置目标配置 → 解决 Turnstile → 抓取产品页面
→ 提取数据 → 比较数据 → 数据变动? → 构建警报 / 无变动
Webhook 路径:
Webhook 触发 → 解决 Turnstile → 抓取产品页面
→ 提取数据 → 比较数据 → 数据变动? → 构建警报 / 无变动
→ 响应 Webhook
关键行为:
Cloudflare Turnstile(非 Cloudflare Challenge)—— 内部使用 AntiTurnstileTaskProxyLesscf-turnstile-response 头随抓取请求发送cf_clearance Cookie 进行指纹匹配$workflow.staticData.lastPrice 实现跨执行持久化websiteURL 和 websiteKey(无需 Set Target Config 节点){
"nodes": [
{
"parameters": {
"content": "## Turnstile 抓取 \u2014 价格 & 产品监控\n\n### 工作原理\n\n1. 工作流程每 6 小时或通过 webhook 触发。\n2. 设置目标配置,包括网站 URL 和 Key。\n3. 解决 Turnstile captcha 以访问产品页面。\n4. 抓取产品数据并提取相关内容。\n5. 比较提取的数据与之前的数据以识别变更。\n6. 若有变更,则生成警报,并对 webhook 请求发送响应。\n\n### 设置步骤\n\n- [ ] 配置调度触发器间隔。\n- [ ] 设置 webhook URL 以实现实时触发。\n- [ ] 确保验证码解决凭证有效。\n- [ ] 配置目标网站 URL 和产品 Key。\n\n### 自定义\n\n调度触发的间隔可根据监控需求调整。",
"width": 480,
"height": 896
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1184,
-240
],
"id": "1824aba9-471e-4052-912e-888d939349df",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 定时抓取触发器\n\n每 6 小时启动工作流程并设置目标配置。",
"width": 512,
"height": 304,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-624,
-80
],
"id": "89b7d6a2-b4bf-4a7d-8667-ce3f31d2eb92",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 定时验证码解决和抓取\n\n基于定时解决验证码并抓取产品页面数据。",
"width": 1472,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16,
-64
],
"id": "f39fc94b-d0ae-4141-9491-fa82702a72fc",
"name": "Sticky Note2"
},
{
"parameters": {
"content": "## 定时数据评估\n\n比较新抓取的数据与之前的数据,检测变更并生成警报。",
"width": 240,
"height": 528,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1536,
-240
],
"id": "0d31be95-f94c-40fd-99cd-7d988109d3f4",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## Webhook 抓取触发器\n\n通过 webhook 触发抓取工作流程,实现实时更新。",
"width": 240,
"height": 352,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-624,
256
],
"id": "e4a50f47-180c-484e-bf70-09a46543e01b",
"name": "Sticky Note4"
},
{
"parameters": {
"content": "## Webhook 验证码解决和抓取\n\n为 webhook 触发解决验证码并抓取产品页面数据。",
"width": 1472,
"height": 272,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
16,
336
],
"id": "1167cf0e-d561-40fa-b366-331a20d99a31",
"name": "Sticky Note5"
},
{
"parameters": {
"content": "## Webhook 数据评估与响应\n\n评估数据变更,并将结果响应给 webhook。",
"width": 672,
"height": 432,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1536,
320
],
"id": "0dab95b9-d762-43cf-9a11-b92212a8c2a7",
"name": "Sticky Note6"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-576,
48
],
"id": "ts-s-01",
"name": "Every 6 Hours"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-001",
"name": "websiteURL",
"value": "https://YOUR-TARGET-SITE.com/product-page",
"type": "string"
},
{
"id": "cfg-002",
"name": "websiteKey",
"value": "YOUR_SITE_KEY_HERE",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-256,
48
],
"id": "ts-s-02",
"name": "设置目标配置 [定时]"
},
{
"parameters": {
"operation": "Cloudflare Turnstile",
"websiteURL": "={{ $json.websiteURL }}",
"websiteKey": "={{ $json.websiteKey }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
64,
48
],
"id": "ts-s-03",
"name": "解决 Turnstile",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver account"
}
}
},
{
"parameters": {
"url": "={{ $('Set Target Config [Schedule]').first().json.websiteURL }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "user-agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
},
{
"name": "cf-turnstile-response",
"value": "={{ $json.data.solution.token }}"
}
]
},
"options": {
"response": {
"response": {}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
384,
48
],
"id": "ts-s-04",
"name": "抓取产品页面"
},
{
"parameters": {
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "price",
"cssSelector": ".product-price, [data-price], .price"
},
{
"key": "productName",
"cssSelector": "h1, .product-title"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
704,
48
],
"id": "ts-s-05",
"name": "提取数据"
},
{
"parameters": {
"jsCode": "const staticData = $workflow.staticData;\nconst currentPrice = $input.first().json.price;\nconst previousPrice = staticData.lastPrice;\nconst productName = $input.first().json.productName || 'Product';\nconst parsePrice = (str) => { if (!str) return null; const match = str.match(/[\\d]+\\.?\\d*/); return match ? parseFloat(match[0].replace(',', '')) : null; };\nconst currentNum = parsePrice(currentPrice);\nconst previousNum = parsePrice(previousPrice);\nstaticData.lastPrice = currentPrice;\nstaticData.lastChecked = new Date().toISOString();\nconst changed = previousNum !== null && currentNum !== null && currentNum !== previousNum;\nconst direction = changed ? (currentNum < previousNum ? 'dropped' : 'increased') : 'unchanged';\nconst diff = changed ? Math.abs(currentNum - previousNum).toFixed(2) : '0';\nreturn [{ json: { productName, currentPrice, previousPrice: previousPrice || '首次检测', changed, direction, diff: changed ? `$${diff}` : null, checkedAt: new Date().toISOString() } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
48
],
"id": "ts-s-06",
"name": "比较数据"
},
{
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"conditions": [
{
"id": "if-1",
"leftValue": "={{ $json.changed }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1344,
48
],
"id": "ts-s-07",
"name": "数据变动?"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a1",
"name": "alert",
"value": "=价格 {{ $json.direction }}:{{ $json.productName }}:{{ $json.previousPrice }} \u2192 {{ $json.currentPrice }}",
"type": "string"
},
{
"id": "a2",
"name": "severity",
"value": "={{ $json.direction === 'dropped' ? 'deal' : 'info' }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1584,
-48
],
"id": "ts-s-08",
"name": "构建警报"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "n1",
"name": "status",
"value": "no_change",
"type": "string"
},
{
"id": "n2",
"name": "currentPrice",
"value": "={{ $json.currentPrice }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1584,
128
],
"id": "ts-s-09",
"name": "无变动"
},
{
"parameters": {
"httpMethod": "POST",
"path": "price-monitor-turnstile",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-576,
448
],
"id": "ts-s-10",
"name": "Webhook 触发",
"webhookId": "6a4f76c7-fc5c-440d-96cb-75c9c3bebcdb",
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "Cloudflare Turnstile",
"websiteURL": "={{ $json.body.websiteURL }}",
"websiteKey": "={{ $json.body.websiteKey }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
64,
448
],
"id": "ts-s-11",
"name": "解决 Turnstile [W]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver account"
}
}
},
{
"parameters": {
"url": "={{ $('Webhook Trigger').item.json.body.websiteURL }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "user-agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
},
{
"name": "cf-turnstile-response",
"value": "={{ $json.data.solution.token }}"
}
]
},
"options": {
"response": {
"response": {}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
384,
448
],
"id": "ts-s-12",
"name": "抓取产品页面 [W]"
},
{
"parameters": {
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "price",
"cssSelector": ".product-price, [data-price], .price"
},
{
"key": "productName",
"cssSelector": "h1, .product-title"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
704,
448
],
"id": "ts-s-13",
"name": "提取数据 [W]"
},
{
"parameters": {
"jsCode": "const staticData = $workflow.staticData;\nconst currentPrice = $input.first().json.price;\nconst previousPrice = staticData.lastPrice;\nconst productName = $input.first().json.productName || 'Product';\nconst parsePrice = (str) => { if (!str) return null; const match = str.match(/[\\d]+\\.?\\d*/); return match ? parseFloat(match[0].replace(',', '')) : null; };\nconst currentNum = parsePrice(currentPrice);\nconst previousNum = parsePrice(previousPrice);\nstaticData.lastPrice = currentPrice;\nstaticData.lastChecked = new Date().toISOString();\nconst changed = previousNum !== null && currentNum !== null && currentNum !== previousNum;\nconst direction = changed ? (currentNum < previousNum ? 'dropped' : 'increased') : 'unchanged';\nconst diff = changed ? Math.abs(currentNum - previousNum).toFixed(2) : '0';\nreturn [{ json: { productName, currentPrice, previousPrice: previousPrice || '首次检测', changed, direction, diff: changed ? `$${diff}` : null, checkedAt: new Date().toISOString() } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1040,
448
],
"id": "ts-s-14",
"name": "比较数据 [W]"
},
{
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"conditions": [
{
"id": "if-2",
"leftValue": "={{ $json.changed }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1344,
448
],
"id": "ts-s-15",
"name": "数据变动? [W]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a4",
"name": "alert",
"value": "=价格 {{ $json.direction }}:{{ $json.productName }}:{{ $json.previousPrice }} \u2192 {{ $json.currentPrice }}",
"type": "string"
},
{
"id": "a5",
"name": "severity",
"value": "={{ $json.direction === 'dropped' ? 'deal' : 'info' }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1584,
432
],
"id": "ts-s-16",
"name": "构建警报 [W]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "n4",
"name": "status",
"value": "no_change",
"type": "string"
},
{
"id": "n5",
"name": "currentPrice",
"value": "={{ $json.currentPrice }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1584,
592
],
"id": "ts-s-17",
"name": "无变动 [W]"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
2064,
544
],
"id": "ts-s-18",
"name": "响应 Webhook"
}
],
"connections": {
"Every 6 Hours": {
"main": [
[
{
"node": "Set Target Config [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Set Target Config [Schedule]": {
"main": [
[
{
"node": "Solve Turnstile",
"type": "main",
"index": 0
}
]
]
},
"Solve Turnstile": {
"main": [
[
{
"node": "Fetch Product Page",
"type": "main",
"index": 0
}
]
]
},
"Fetch Product Page": {
"main": [
[
{
"node": "Extract Data",
"type": "main",
"index": 0
}
]
]
},
"Extract Data": {
"main": [
[
{
"node": "Compare Data",
"type": "main",
"index": 0
}
]
]
},
"Compare Data": {
"main": [
[
{
"node": "Data Changed?",
"type": "main",
"index": 0
}
]
]
},
"Data Changed?": {
"main": [
[
{
"node": "Build Alert",
"type": "main",
"index": 0
}
],
[
{
"node": "No Change",
"type": "main",
"index": 0
}
]
]
},
"Webhook Trigger": {
"main": [
[
{
"node": "Solve Turnstile [W]",
"type": "main",
"index": 0
}
]
]
},
"Solve Turnstile [W]": {
"main": [
[
{
"node": "Fetch Product Page [W]",
"type": "main",
"index": 0
}
]
]
},
"Fetch Product Page [W]": {
"main": [
[
{
"node": "Extract Data [W]",
"type": "main",
"index": 0
}
]
]
},
"Extract Data [W]": {
"main": [
[
{
"node": "Compare Data [W]",
"type": "main",
"index": 0
}
]
]
},
"Compare Data [W]": {
"main": [
[
{
"node": "Data Changed? [W]",
"type": "main",
"index": 0
}
]
]
},
"Data Changed? [W]": {
"main": [
[
{
"node": "Build Alert [W]",
"type": "main",
"index": 0
}
],
[
{
"node": "No Change [W]",
"type": "main",
"index": 0
}
]
]
},
"Build Alert [W]": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"No Change [W]": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
此工作流实现对受 Turnstile 保护的网站的自动登录。它遵循与上文 Cloudflare Challenge 账户登录相同的双触发模式(定时 + webhook)——但没有使用 TLS 服务器、代理或自定义代码节点。
与 Cloudflare Challenge 登录的主要区别: 无需 TLS 服务器,无需代理,无需
Prepare TLS Login Request代码节点。Turnstile token 作为cf-turnstile-response表单字段,直接通过 n8n 内置的 HTTP 请求节点提交。凭证以标准的form-urlencoded请求体参数形式发送。
定时路径:
每 24 小时 → 设置登录配置 → 解决 Turnstile → 提交登录
→ 登录成功? → 标记成功 / 标记失败
Webhook 路径:
Webhook 触发 → 解决 Turnstile → 提交登录
→ 登录成功? → 标记成功 / 标记失败 → 回复 Webhook
关键行为:
Cloudflare Turnstile(非 Cloudflare Challenge)——使用 AntiTurnstileTaskProxyLess,不需要代理cf-turnstile-response 表单字段提交(非 CF Challenge 中的 cookie 头)form-urlencoded 格式提交,包含 email、password 和 cf-turnstile-response 字段statusCode < 400 且响应体中包含 successMarker 来判断——与 CF Challenge 登录一致的模式usernameValue、passwordValue、usernameField、passwordField、loginActionURL、successMarkername 参数是表达式(例如 $('Webhook Trigger').item.json.body.usernameField || 'email'),调用者可以指定其站点的字段名{
"nodes": [
{
"parameters": {
"content": "## Turnstile 账户登录\n\n### 工作原理\n\n1. 使用定时器每 24 小时触发一次登录流程。\n2. 设置登录配置并解决 Turnstile 验证。\n3. 提交登录表单,检测登录是否成功。\n4. 在定时流程中标记登录尝试为成功或失败。\n5. 另外,可通过 webhook 触发启动登录流程,并执行类似的步骤。\n\n### 配置步骤\n\n- [ ] 通过设置调度器实现周期性执行。\n- [ ] 配置 webhook 端点以支持外部触发。\n- [ ] 设置登录凭据和 URL 配置。\n\n### 自定义\n\n根据需要调整调度器时间或修改 webhook 响应处理。",
"width": 480,
"height": 896
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1168,
-160
],
"id": "8ca80c21-2de5-41e7-b3e4-de184fc1d8fe",
"name": "便签"
},
{
"parameters": {
"content": "## 定时登录流程\n\n每 24 小时触发以执行登录步骤",
"width": 1920,
"height": 448,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-608,
-160
],
"id": "533be07a-6fe7-4ada-a40c-7a7749ba968d",
"name": "便签1"
},
{
"parameters": {
"content": "## Webhook 登录流程\n\n处理由 webhook 触发的登录请求",
"width": 2288,
"height": 416,
"color": 7
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-608,
320
],
"id": "c6a8e296-2bfb-4ffa-8819-aa420e936589",
"name": "便签2"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-560,
48
],
"id": "ts-l-01",
"name": "每 24 小时"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "l1",
"name": "websiteURL",
"value": "https://YOUR-LOGIN-PAGE.com",
"type": "string"
},
{
"id": "l2",
"name": "websiteKey",
"value": "YOUR_SITE_KEY_HERE",
"type": "string"
},
{
"id": "l3",
"name": "successMarker",
"value": "account-dashboard",
"type": "string"
},
{
"id": "l4",
"name": "userAgent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-240,
48
],
"id": "ts-l-02",
"name": "设置登录配置 [定时]"
},
{
"parameters": {
"operation": "Cloudflare Turnstile",
"websiteURL": "={{ $json.websiteURL }}",
"websiteKey": "={{ $json.websiteKey }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
256,
48
],
"id": "ts-l-03",
"name": "解决 Turnstile [定时]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver 账户"
}
}
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Set Login Config [Schedule]').item.json.websiteURL }}/login",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "content-type",
"value": "application/x-www-form-urlencoded"
},
{
"name": "user-agent",
"value": "={{ $('Set Login Config [Schedule]').item.json.userAgent }}"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "email",
"value": "your-email@example.com"
},
{
"name": "password",
"value": "YOUR_ACCOUNT_PASSWORD"
},
{
"name": "cf-turnstile-response",
"value": "={{ $json.data.solution.token }}"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
576,
48
],
"id": "ts-l-04",
"name": "提交登录 [定时]"
},
{
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"conditions": [
{
"id": "lif1",
"leftValue": "={{ $json.statusCode < 400 && String($json.body || $json.data || '').includes($('Set Login Config [Schedule]').item.json.successMarker) }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
896,
48
],
"id": "ts-l-05",
"name": "登录成功? [定时]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "s1",
"name": "status",
"value": "success",
"type": "string"
},
{
"id": "s2",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1168,
-48
],
"id": "ts-l-06",
"name": "标记成功"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "f1",
"name": "status",
"value": "failed",
"type": "string"
},
{
"id": "f2",
"name": "statusCode",
"value": "={{ $json.statusCode }}",
"type": "number"
},
{
"id": "f3",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1168,
128
],
"id": "ts-l-07",
"name": "标记失败"
},
{
"parameters": {
"httpMethod": "POST",
"path": "account-login-turnstile",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-560,
544
],
"id": "ts-l-08",
"name": "Webhook 触发",
"webhookId": "9c7a53a4-d3ee-495b-9381-3a9425bb1b36"
},
{
"parameters": {
"operation": "Cloudflare Turnstile",
"websiteURL": "={{ $json.body.websiteURL }}",
"websiteKey": "={{ $json.body.websiteKey }}",
"optional": {}
},
"type": "n8n-nodes-capsolver.capSolver",
"typeVersion": 1,
"position": [
256,
544
],
"id": "ts-l-09",
"name": "解决 Turnstile [Webhook]",
"credentials": {
"capSolverApi": {
"id": "BeBFMAsySMsMGeE9",
"name": "CapSolver 账户"
}
}
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Webhook Trigger').item.json.body.loginActionURL }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "content-type",
"value": "application/x-www-form-urlencoded"
},
{
"name": "user-agent",
"value": "={{ $('Webhook Trigger').item.json.body.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' }}"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "={{ $('Webhook Trigger').item.json.body.usernameField || 'email' }}",
"value": "={{ $('Webhook Trigger').item.json.body.usernameValue }}"
},
{
"name": "={{ $('Webhook Trigger').item.json.body.passwordField || 'password' }}",
"value": "={{ $('Webhook Trigger').item.json.body.passwordValue }}"
},
{
"name": "cf-turnstile-response",
"value": "={{ $json.data.solution.token }}"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
576,
544
],
"id": "ts-l-10",
"name": "提交登录 [Webhook]"
},
{
"parameters": {
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"conditions": [
{
"id": "lif2",
"leftValue": "={{ $json.statusCode < 400 && String($json.body || $json.data || '').includes($('Webhook Trigger').item.json.body.successMarker) }}",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
896,
544
],
"id": "ts-l-11",
"name": "登录成功? [Webhook]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "ws1",
"name": "status",
"value": "success",
"type": "string"
},
{
"id": "ws2",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1168,
432
],
"id": "ts-l-12",
"name": "标记成功 [W]"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "wf1",
"name": "status",
"value": "failed",
"type": "string"
},
{
"id": "wf2",
"name": "statusCode",
"value": "={{ $json.statusCode }}",
"type": "number"
},
{
"id": "wf3",
"name": "checkedAt",
"value": "={{ new Date().toISOString() }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1168,
576
],
"id": "ts-l-13",
"name": "标记失败 [W]"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1536,
544
],
"id": "ts-l-14",
"name": "回复 Webhook"
}
],
"connections": {
"Every 24 Hours": {
"main": [
[
{
"node": "Set Login Config [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Set Login Config [Schedule]": {
"main": [
[
{
"node": "Solve Turnstile [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Solve Turnstile [Schedule]": {
"main": [
[
{
"node": "Submit Login [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Submit Login [Schedule]": {
"main": [
[
{
"node": "Login OK? [Schedule]",
"type": "main",
"index": 0
}
]
]
},
"Login OK? [Schedule]": {
"main": [
[
{
"node": "Mark Success",
"type": "main",
"index": 0
}
],
[
{
"node": "Mark Failed",
"type": "main",
"index": 0
}
]
]
},
"Webhook Trigger": {
"main": [
[
{
"node": "Solve Turnstile [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Solve Turnstile [Webhook]": {
"main": [
[
{
"node": "Submit Login [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Submit Login [Webhook]": {
"main": [
[
{
"node": "Login OK? [Webhook]",
"type": "main",
"index": 0
}
]
]
},
"Login OK? [Webhook]": {
"main": [
[
{
"node": "Mark Success [W]",
"type": "main",
"index": 0
}
],
[
{
"node": "Mark Failed [W]",
"type": "main",
"index": 0
}
]
]
},
"Mark Success [W]": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Mark Failed [W]": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "962ff0267b713be0344b866fa54daae28de8ed2144e2e6867da355dae193ea1f"
}
}
你已经在 n8n 中构建了一个完整的 Cloudflare Challenge 绕过流水线——无需浏览器自动化、无需 Puppeteer、无需 Playwright。仅需三个组件协同工作:CapSolver 用于解决挑战、一个 Go TLS 服务器用于伪装 Chrome 的网络指纹,以及一个 n8n 工作流来协调所有环节。
关键见解是,解决挑战只是问题的一半。没有在后续抓取中匹配 TLS 指纹,cf_clearance 就毫无作用——Cloudflare 检查的是握手过程,而不仅仅是 Cookie。httpcloak TLS 服务器处理了这层,使抓取在网络层面上无法区分于真实的 Chrome 浏览器。
该仓库现在为你提供了用于 Cloudflare 保护站点的实际起始模板:
cf_clearance + userAgentSolver API 是最简单的切入点——4 个节点,无需 TLS 服务器。Turnstile 工作流则更简单——完全不需要代理或 TLS 服务器,因为 Turnstile 返回的是令牌(非 IP 绑定的 Cookie),且不做 TLS 指纹识别。对于直接抓取页面的 Cloudflare Challenge 工作流,CapSolver 解决挑战,TLS 服务器发出实际请求。配置好占位符,保持工作流未激活直到匹配目标,然后再激活。
提示: 这些工作流使用调度 + webhook 触发器,但你可以将触发节点替换成任何 n8n 触发器——手动、应用事件、表单提交等。抓取数据后,使用 n8n 内置节点将结果保存到 Google Sheets、数据库、云存储,或通过 Telegram/Slack/Email 发送提醒。
准备好开始了吗? 注册 CapSolver 并使用优惠码 n8n,首次充值享额外 8% 奖励!

AntiCloudflareTask 解决的是完整的 Cloudflare Bot Management 挑战——那种完全阻止访问的网站“Just a moment…”屏幕。它需要代理,因为 CapSolver 必须通过浏览器加载实际受保护页面。AntiTurnstileTaskProxyLess 解决嵌入代码中的 Turnstile 小部件(登录表单、注册表单等),不需要代理。挑战不同,任务类型也不同。
n8n 的 HTTP Request 节点使用的是 Go 标准库的 net/http,该库的 TLS 指纹与众不同,容易被 Cloudflare 识别。即使拥有有效的 cf_clearance Cookie,如果请求的 TLS 指纹不符合已知浏览器对应的指纹,Cloudflare 仍会重新发起挑战。TLS 服务器通过使用 httpcloak 来伪装成真实的 Chrome TLS 堆栈解决了这个问题。
Cloudflare 的机器人评分会对 IP 地址赋予风险分数。数据中心 IP(如 AWS、GCP、VPS 提供商等)是众所周知的高风险 IP。AntiCloudflareTask 使用你的代理加载挑战页面,如果 Cloudflare 识别该 IP 是数据中心,它要么提供 CapSolver 无法解决的更难挑战,要么直接挑战失败。住宅和移动 IP 风险分数较低,更可靠通过。
不需要。Turnstile 本质上与 Cloudflare Challenge 不同。Turnstile 返回一个短期有效的 token,作为请求头或表单字段提交——它不绑定特定 IP 或 TLS 指纹。CapSolver 使用 AntiTurnstileTaskProxyLess 解决 Turnstile,完全不需要代理。由于发送的是令牌(非 IP 绑定 Cookie),n8n 内置 HTTP Request 节点可以正常使用——无需伪装 TLS 指纹。
cf_clearance 会过期吗?会的。过期时间取决于网站的 Cloudflare 配置,可能从几分钟到 24 小时不等。对于定期抓取任务,调度工作流(每 6 小时)会定期重新解决挑战。对于按需抓取,webhook 路径会在每次请求时解决新的挑战。