Crawl4AI v0.7.6 实战:Docker 任务队列 API 的 Webhook 通知机制详解
Crawl4AI v0.7.6(2025-10-22 发布)的核心主题是:为 Docker 部署下的异步任务队列 API(/crawl/job 与 /llm/job)引入完整的 Webhook 通知基础设施。读完本文,你将掌握如何用 Webhook 替代轮询、如何配置全局与单任务级别的回调参数、如何编写幂等的接收端,并能从 deploy/docker/webhook.py 等源码理解其指数退避重试、头部安全过滤与 SSRF 防护的底层实现。
从轮询到事件驱动:v0.7.6 解决了什么问题
在 v0.7.6 之前,客户端提交异步任务后只能反复轮询任务状态接口,等待 status 变为 completed:
# 旧方式:轮询
# 提交任务
response = requests.post("http://localhost:11235/crawl/job", json=payload)
task_id = response.json()['task_id']
# 循环查询直到完成
while True:
status = requests.get(f"http://localhost:11235/crawl/job/{task_id}")
if status.json()['status'] == 'completed':
break
time.sleep(5) # 等待后重试
这种模式存在三重代价:固定的轮询延迟(结果实际可能早已完成)、持续占用客户端连接与服务器带宽,以及在高并发任务量下对 API 服务器造成的无谓压力。v0.7.6 的答案是——任务完成后由服务端主动 POST 一个 JSON 通知到你的回调地址:
# 新方式:Webhook
# 提交任务时附带 webhook 配置
payload = {
"urls": ["https://example.com"],
"webhook_config": {
"webhook_url": "https://myapp.com/webhook",
"webhook_data_in_payload": True
}
}
response = requests.post("http://localhost:11235/crawl/job", json=payload)
# 无需再轮询:任务完成时你的 webhook 处理器会自动收到结果
这一版本的关键能力可以归纳为六条,全部可在源码中得到印证:
| 能力 | 说明 | 源码依据 |
|---|---|---|
| 通用 Webhook 支持 | /crawl/job 与 /llm/job 两类任务均可回调 |
api.py 中两条任务处理链路均调用 notify_job_completion |
| 灵活的投递模式 | 仅通知,或在 payload 中携带完整结果数据 | webhook.py 中 data_in_payload 分支 |
| 可靠投递 | 指数退避重试(5 次:1s → 2s → 4s → 8s → 16s) | webhook.py send_webhook 的退避循环 |
| 自定义认证头 | 通过 webhook_headers 添加如 X-Webhook-Secret |
schemas.py 的 WebhookConfig 模型 |
| 全局默认配置 | 在 config.yml 中设置默认 webhook URL 等 |
config.yml 的 webhooks 段 |
| 任务类型标识 | payload 中以 task_type 区分 crawl 与 llm_extraction |
webhook.py notify_job_completion 组装逻辑 |
两类任务的 Webhook 用法
Crawl 任务
以 curl 提交一个带完整 webhook_config 的抓取任务:
curl -X POST http://localhost:11235/crawl/job \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com"],
"browser_config": {"headless": true},
"crawler_config": {"cache_mode": "bypass"},
"webhook_config": {
"webhook_url": "https://myapp.com/webhooks/crawl-complete",
"webhook_data_in_payload": false,
"webhook_headers": {
"X-Webhook-Secret": "your-secret-token"
}
}
}'
webhook_data_in_payload: false 表示"仅通知"模式:payload 只包含任务元信息,接收端随后用 task_id 回查结果接口(如 GET http://localhost:11235/crawl/job/{task_id})拉取完整数据。对于大体积的抓取结果,推荐这种模式以避免超大 webhook body。
LLM 抽取任务(本版新增)
/llm/job 端点同样支持 webhook,这意味着"提交抽取任务 → 继续处理其他工作 → 完成后收到结构化结果"的异步管线成立:
curl -X POST http://localhost:11235/llm/job \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/article",
"q": "Extract the article title, author, and publication date",
"schema": "{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"}}}",
"provider": "openai/gpt-4o-mini",
"webhook_config": {
"webhook_url": "https://myapp.com/webhooks/llm-complete",
"webhook_data_in_payload": true
}
}'
从 api.py 的 process_llm_extraction 实现看,无论任务成功(provider 校验失败、抓取失败、抽取结果解析)还是抛异常,都会走到同一处 webhook_service.notify_job_completion(...) 调用点并传入 task_type="llm_extraction"——即失败路径同样会触发回调,只是 status 为 failed。这是一个容易被忽视但对监控告警场景很关键的保证。
Webhook Payload 结构
服务端回调发送的 JSON 由 schemas.py 中的 WebhookPayload 模型定义,与 webhook.py 中 notify_job_completion 实际组装的字段一一对应。
成功(携带数据):
{
"task_id": "llm_1698765432",
"task_type": "llm_extraction",
"status": "completed",
"timestamp": "2025-10-22T10:30:00.000000+00:00",
"urls": ["https://example.com/article"],
"data": {
"extracted_content": {
"title": "Understanding Web Scraping",
"author": "John Doe",
"date": "2025-10-22"
}
}
}
失败:
{
"task_id": "crawl_abc123",
"task_type": "crawl",
"status": "failed",
"timestamp": "2025-10-22T10:30:00.000000+00:00",
"urls": ["https://example.com"],
"error": "Connection timeout after 30s"
}
几个值得注意的实现细节:
timestamp为 UTC 的 ISO 8601 格式,由datetime.now(timezone.utc).isoformat()生成;data字段仅在webhook_data_in_payload=True且存在结果数据时出现,LLM 抽取场景下其结构固定为{"extracted_content": ...}(api.py中result_data = {"extracted_content": content});error字段仅在有错误时出现,接收端应使用payload.get('error')这类宽容取值方式。
编写一个 Webhook 接收端
一个最小可用的 Flask 接收器示例,覆盖"携带数据"与"仅通知"两种模式:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
payload = request.json
task_id = payload['task_id']
task_type = payload['task_type']
status = payload['status']
if status == 'completed':
if 'data' in payload:
# 数据随通知一起送达,直接处理
data = payload['data']
else:
# 仅通知模式:回查 API 拉取结果
endpoint = 'crawl' if task_type == 'crawl' else 'llm'
response = requests.get(f'http://localhost:11235/{endpoint}/job/{task_id}')
data = response.json()
# 在此编写你的业务逻辑
print(f"Job {task_id} completed!")
elif status == 'failed':
error = payload.get('error', 'Unknown error')
print(f"Job {task_id} failed: {error}")
return jsonify({"status": "received"}), 200
app.run(port=8080)
注意最后两行:接收端必须返回 2xx 状态码,服务端将 200–299 视为投递成功(见下文重试语义)。
重试机制与投递语义(源码级解读)
webhook.py 的 WebhookDeliveryService.send_webhook 实现了完整的重试策略,理解它对编写健壮的接收端很有帮助:
1. 指数退避循环。 最多 max_attempts 次尝试(默认 5),每次失败后按 min(initial_delay * 2^attempt, max_delay) 休眠,即 1s → 2s → 4s → 8s → 16s,且不会超过 max_delay(默认 32s)。每次 HTTP 调用的总超时由 timeout_ms(默认 30s)控制。
2. 区分客户端错误与服务端错误。 源码中的判断逻辑是:状态码落在 200–299 则成功返回;状态码低于 500(4xx)视为"目标明确拒绝",不重试直接放弃;只有 5xx、连接异常等情况才进入退避重试。换句话说,如果你的接收端因为 bug 返回 500,webhook 会在约 31 秒内(累计退避时间)被重发 5 次;如果返回 403(例如密钥校验失败),则一次都不会重试——排障时留意这一区别。
3. 头部的安全过滤。 用户通过 webhook_headers 传入的自定义头会先经过 sanitize_webhook_headers 校验:头名只允许 A-Za-z0-9-(最长 64 字符)、最多 20 个头、头值最长 2048 字节且禁止 CRLF/NUL 控制字符,并且 authorization、cookie、content-length、transfer-encoding 等敏感/逐跳头一律被拒绝。该校验在请求入口(schemas.py 的 WebhookConfig Pydantic field_validator,非法请求直接 422)和投递前(webhook.py 中二次清洗)各执行一次,属于纵深防御设计。全局 config.yml 里的 headers(如 User-Agent: "Crawl4AI-Webhook/1.0")会与自定义头合并,Content-Type 固定为 application/json。
4. SSRF 防护。 从源码结构看,投递前会先解析并"钉住"目标域名对应的 IP(resolve_and_pin + 自定义 aiohttp resolver),TLS 证书校验仍针对原始域名进行,从而在保持证书校验完整性的同时关闭 DNS rebinding 攻击面;跟随重定向时(最多 5 跳)每一跳都会重新做出口校验,重定向到内网目标会被直接阻断且不再重试。如果你的 webhook 服务经过代理返回 3xx,需要确保目标仍是可达的全局地址。
5. 配置的持久化。 提交任务时,webhook_config 会被 JSON 序列化后写入 Redis 任务记录(api.py 中 task_data["webhook_config"] = json.dumps(webhook_config)),保证后台 worker 在执行任务、稍后执行通知时能取回完整的回调配置。
全局配置:config.yml 中的 webhooks 段
不希望在每个任务里重复书写回调地址,可以在服务端 config.yml 中设置默认值。当前仓库 config.yml 中的实际配置段如下:
# Webhook Configuration
webhooks:
enabled: true
default_url: null # Optional: default webhook URL for all jobs
data_in_payload: false # Optional: default behavior for including data
retry:
max_attempts: 5
initial_delay_ms: 1000 # 1s, 2s, 4s, 8s, 16s exponential backoff
max_delay_ms: 32000
timeout_ms: 30000 # 30s timeout per webhook call
headers: # Optional: default headers to include
User-Agent: "Crawl4AI-Webhook/1.0"
各字段的优先级与回退规则(由 webhook.py notify_job_completion 实现):
| 配置项 | 作用 | 优先级 |
|---|---|---|
enabled |
总开关,设为 false 时所有通知静默跳过 |
全局 |
default_url |
未提供 webhook_config 时的兜底回调地址 |
全局(任务级 webhook_url 优先) |
data_in_payload |
是否将结果数据放入 payload 的默认值 | 全局(任务级 webhook_data_in_payload 优先) |
retry.* |
重试次数与退避参数,默认值即上表数值 | 全局 |
headers |
所有 webhook 请求附带的默认头 | 全局(任务级自定义头合并覆盖) |
值得强调的是:没有配置任何 webhook URL 时,通知逻辑直接跳过(debug 日志),任务流程完全不受影响——这正是 v0.7.6 能做到零破坏性升级的原因。
升级与迁移指南
本版本无任何破坏性变更:webhook_config 为可选字段,存量代码与轮询用法原样可用。
Docker 方式:
# 拉取最新镜像
docker pull unclecode/crawl4ai:0.7.6
# 或使用 latest 标签
docker pull unclecode/crawl4ai:latest
# 启动容器(webhook 功能随镜像自带)
docker run -d \
-p 11235:11235 \
--env-file .llm.env \
--name crawl4ai \
unclecode/crawl4ai:0.7.6
Python 包方式:
pip install --upgrade crawl4ai
迁移只需要一步:在现有任务 payload 中追加 webhook_config 字段:
# 在现有 payload 中追加即可
payload = {
# 原有配置保持不变
"urls": ["https://example.com"],
"browser_config": {...},
"crawler_config": {...},
# 新增:webhook 配置
"webhook_config": {
"webhook_url": "https://myapp.com/webhook",
"webhook_data_in_payload": True
}
}
实战技巧
- 大结果用"仅通知"模式:
webhook_data_in_payload: false,接收端再凭task_id回查,避免巨型 webhook body 拖慢回调链路; - 用自定义头做认证与追踪:如
X-Webhook-Secret做共享密钥校验、X-Request-Id做链路追踪(注意受前述头部白名单限制); - 配置全局默认 webhook:所有任务统一落点,便于集中处理与审计;
- 接收端必须幂等:5xx 会触发重试,同一 webhook 可能被投递多次,处理逻辑需容忍重复(例如按
task_id + status去重); - LLM 抽取配合结构化 schema:定义清晰 JSON Schema,让
data.extracted_content成为下游可直接消费的可预测结构。
本版本还顺带修复了三处 webhook 相关缺陷(见发布说明):Pydantic HttpUrl 字段的配置序列化问题、投递服务的错误处理加固、以及 Redis 任务存储中 webhook 配置的持久化增强——后者正是上文"配置持久化"细节对应的修复项。
进一步探索的仓库资源
- WEBHOOK_EXAMPLES.md:官方完整 webhook 用法示例集(仅通知、携带数据、自定义头等场景的 curl 请求/响应对照);
- docker_webhook_example.py:可运行的端到端代码示例;
- demo_v0.7.6.py:本版本发布演示,覆盖 crawl/LLM 两类 webhook、自定义头、重试机制与实时接收器,可用
python docs/releases_review/demo_v0.7.6.py运行; - webhook.py 与 schemas.py:投递服务与安全校验的完整源码;
- Docker README:Docker 部署文档中补充的 webhook 章节。
对于事件驱动的微服务架构,这套机制的价值在于彻底解耦"任务提交"与"结果处理":提交方无需保持长连接,处理方无需感知轮询节奏,而自动重试与失败回调(failed 通知)共同构成了异步管线的可靠性底线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00