crw-watch
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesecrw-watch — change tracking and diffing
crw-watch — 变更追踪与差异对比
When to use
适用场景
- You want to know what changed between two snapshots of a page.
- Step 7 in the crw ladder. Assumes you can already scrape the page — see crw-scrape (step 2).
- You want a self-hosted, stateless diff primitive you control. Firecrawl offers change tracking only as a managed cloud feature; crw exposes the same primitive as a REST endpoint that runs on your own infra — you own the snapshots, the cadence, and the data.
- 你想了解同一页面的两个快照之间具体有哪些变更。
- 这是crw阶梯中的第7步。假设你已经能够抓取页面内容——可查看crw-scrape(第2步)。
- 你需要一个由自己掌控的自托管、无状态差异对比原语。Firecrawl仅将变更追踪作为托管云功能提供;而crw将相同的原语以REST端点的形式暴露,运行在你自己的基础设施上——你拥有快照、执行节奏和数据的控制权。
Architecture: crw is stateless
架构:crw是无状态的
crw stores nothing between calls. The caller owns the snapshots:
1. Scrape now → store snapshot (markdown / json)
2. Scrape later → call /v1/change-tracking/diff with current + previous
3. On status=changed → alert / act
4. Repeat on a croncrw不会在调用之间存储任何数据,调用方拥有快照的所有权:
1. 立即抓取页面 → 存储快照(markdown / json格式)
2. 稍后再次抓取 → 调用/v1/change-tracking/diff接口,传入当前快照与历史快照
3. 当status=changed时 → 触发告警/执行操作
4. 通过cron重复执行上述步骤Diff modes
差异对比模式
Two modes, composable:
| Mode | Wire string | What it produces |
|---|---|---|
| Git-style text diff | | Unified-diff text + parse-diff AST in |
| Per-field JSON diff | | Path-keyed map |
Default (omit ): . Combine both: .
modes["gitDiff"]"modes": ["gitDiff", "json"]两种可组合的模式:
| 模式 | 标识字符串 | 输出内容 |
|---|---|---|
| Git风格文本差异 | | 统一差异文本 + 可解析的diff抽象语法树,分别在 |
| 按字段JSON差异 | | 路径键映射 |
默认模式(省略参数):。同时启用两种模式:。
modes["gitDiff"]"modes": ["gitDiff", "json"]Quick start
快速开始
Single page diff (REST):
bash
curl -X POST "$CRW_API_URL/v1/change-tracking/diff" \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"current": {
"markdown": "# Pricing\nPro plan: $49/mo"
},
"previous": {
"markdown": "# Pricing\nPro plan: $39/mo",
"contentHash": "<hash from prior result>"
},
"modes": ["gitDiff"]
}'Response shape:
json
{
"success": true,
"data": {
"status": "changed",
"firstObservation": false,
"contentHash": "<new hash>",
"snapshot": { "markdown": "...", "contentHash": "..." },
"diff": {
"text": "@@ -1,2 +1,2 @@\n # Pricing\n-Pro plan: $39/mo\n+Pro plan: $49/mo",
"json": { "files": [...] }
}
}
}Batch diff (discriminated by presence of key):
batchbash
curl -X POST "$CRW_API_URL/v1/change-tracking/diff" \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"batch": [
{ "url": "https://example.com/pricing", "current": {"markdown": "..."}, "previous": {"markdown": "..."} },
{ "url": "https://example.com/about", "current": {"markdown": "..."} }
],
"modes": ["gitDiff"]
}'Shared /// at the top level are defaults;
each batch item can override them individually.
modesschemapromptcontentTypeInline during a scrape — pass as a format on :
changeTracking/v1/scrapebash
curl -X POST "$CRW_API_URL/v1/scrape" \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"formats": ["markdown", "changeTracking"],
"changeTracking": {
"modes": ["gitDiff"],
"previous": { "markdown": "...", "contentHash": "..." }
}
}'单页面差异对比(REST方式):
bash
curl -X POST "$CRW_API_URL/v1/change-tracking/diff" \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"current": {
"markdown": "# Pricing\nPro plan: $49/mo"
},
"previous": {
"markdown": "# Pricing\nPro plan: $39/mo",
"contentHash": "<hash from prior result>"
},
"modes": ["gitDiff"]
}'响应格式:
json
{
"success": true,
"data": {
"status": "changed",
"firstObservation": false,
"contentHash": "<new hash>",
"snapshot": { "markdown": "...", "contentHash": "..." },
"diff": {
"text": "@@ -1,2 +1,2 @@\n # Pricing\n-Pro plan: $39/mo\n+Pro plan: $49/mo",
"json": { "files": [...] }
}
}
}批量差异对比(通过字段区分):
batchbash
curl -X POST "$CRW_API_URL/v1/change-tracking/diff" \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"batch": [
{ "url": "https://example.com/pricing", "current": {"markdown": "..."}, "previous": {"markdown": "..."} },
{ "url": "https://example.com/about", "current": {"markdown": "..."} }
],
"modes": ["gitDiff"]
}'顶层的///为默认配置;每个批量任务项可单独覆盖这些配置。
modesschemapromptcontentType抓取时内嵌对比 — 在调用时传入作为格式参数:
/v1/scrapechangeTrackingbash
curl -X POST "$CRW_API_URL/v1/scrape" \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"formats": ["markdown", "changeTracking"],
"changeTracking": {
"modes": ["gitDiff"],
"previous": { "markdown": "...", "contentHash": "..." }
}
}'Request fields
请求字段
Single mode:
{ current, previous?, modes, schema?, prompt?, contentType?, tag?, goal?, judgeEnabled? }Batch mode: where each item is
.
{ batch: [...items], modes, schema?, ... }{ url?, current, previous?, modes?, schema?, ... }| Field | Type | Notes |
|---|---|---|
| string | Current page content (gitDiff / mixed) |
| object | Current extracted JSON (json / mixed) |
| string | Prior snapshot for gitDiff |
| string | Persist from prior result's |
| string[] | |
| JSON Schema | Required for |
| string | Natural-language extraction prompt (alternative to |
| string | If binary/non-text, triggers byte-hash comparison only |
| string | Opaque caller ID echoed back on the result |
| string | Natural-language filter for meaningful changes (AI judge, M2) |
| bool | Enable AI judgment (M2 feature; accepted but not yet applied) |
单任务模式:
{ current, previous?, modes, schema?, prompt?, contentType?, tag?, goal?, judgeEnabled? }批量模式: ,其中每个任务项为。
{ batch: [...items], modes, schema?, ... }{ url?, current, previous?, modes?, schema?, ... }| 字段 | 类型 | 说明 |
|---|---|---|
| 字符串 | 当前页面内容(适用于gitDiff / 混合模式) |
| 对象 | 当前提取的JSON数据(适用于json / 混合模式) |
| 字符串 | gitDiff模式所需的历史快照 |
| 字符串 | 从之前请求结果的 |
| 字符串数组 | 默认值 |
| JSON Schema | json模式必填;用于定义需追踪的字段 |
| 字符串 | 自然语言提取提示词(作为 |
| 字符串 | 如果是二进制/非文本内容,仅触发字节哈希对比 |
| 字符串 | 不透明的调用方ID,将在结果中原样返回 |
| 字符串 | 用于定义“有意义变更”的自然语言过滤器(AI判断功能,M2版本) |
| 布尔值 | 启用AI判断功能(M2版本特性;当前已支持参数传入但暂未生效) |
The goal
field (AI judge)
goalgoal
字段(AI判断)
goalgoal- Be specific: beats
"Alert when the listed price changes; ignore copy rewrites and nav updates"."detect important changes" - Narrow the scope: .
"Only flag changes to the Features table, not the hero section" - The judge returns in the result's
{meaningful, confidence, reason, meaningfulChanges[]}field.judgment
goal- 描述要具体:比
"当列出的价格变更时触发告警;忽略文案改写和导航更新"效果更好。"检测重要变更" - 缩小范围:。
"仅标记Features表格的变更,忽略hero区域的更新" - 判断结果将在返回数据的字段中包含
judgment。{meaningful, confidence, reason, meaningfulChanges[]}
Cron pattern (self-hosted)
Cron定时脚本(自部署场景)
bash
#!/usr/bin/env bashbash
#!/usr/bin/env bashcron-check.sh — run every hour via cron or a scheduler
cron-check.sh — 通过cron或调度器每小时执行一次
SNAPSHOT_FILE=".crw/snapshot.json"
CURRENT=$(crw scrape "https://example.com/pricing" --format markdown)
if [ -f "$SNAPSHOT_FILE" ]; then
PREV_MARKDOWN=$(jq -r '.markdown' "$SNAPSHOT_FILE")
PREV_HASH=$(jq -r '.contentHash' "$SNAPSHOT_FILE")
RESULT=$(curl -s -X POST "$CRW_API_URL/v1/change-tracking/diff"
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"previous":{"markdown":$(jq -Rsc . <<<"$PREV_MARKDOWN"),"contentHash":"$PREV_HASH"},"modes":["gitDiff"]}") STATUS=$(echo "$RESULT" | jq -r '.data.status') if [ "$STATUS" = "changed" ]; then echo "CHANGED: $(echo "$RESULT" | jq -r '.data.diff.text')" # → send alert, write to DB, trigger webhook, etc. fi echo "$RESULT" | jq '.data.snapshot' > "$SNAPSHOT_FILE" else
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"previous":{"markdown":$(jq -Rsc . <<<"$PREV_MARKDOWN"),"contentHash":"$PREV_HASH"},"modes":["gitDiff"]}") STATUS=$(echo "$RESULT" | jq -r '.data.status') if [ "$STATUS" = "changed" ]; then echo "CHANGED: $(echo "$RESULT" | jq -r '.data.diff.text')" # → send alert, write to DB, trigger webhook, etc. fi echo "$RESULT" | jq '.data.snapshot' > "$SNAPSHOT_FILE" else
First observation — store the snapshot
curl -s -X POST "$CRW_API_URL/v1/change-tracking/diff"
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"modes":["gitDiff"]}"
| jq '.data.snapshot' > "$SNAPSHOT_FILE" fi
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"modes":["gitDiff"]}"
| jq '.data.snapshot' > "$SNAPSHOT_FILE" fi
undefinedSNAPSHOT_FILE=".crw/snapshot.json"
CURRENT=$(crw scrape "https://example.com/pricing" --format markdown)
if [ -f "$SNAPSHOT_FILE" ]; then
PREV_MARKDOWN=$(jq -r '.markdown' "$SNAPSHOT_FILE")
PREV_HASH=$(jq -r '.contentHash' "$SNAPSHOT_FILE")
RESULT=$(curl -s -X POST "$CRW_API_URL/v1/change-tracking/diff"
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"previous":{"markdown":$(jq -Rsc . <<<"$PREV_MARKDOWN"),"contentHash":"$PREV_HASH"},"modes":["gitDiff"]}") STATUS=$(echo "$RESULT" | jq -r '.data.status') if [ "$STATUS" = "changed" ]; then echo "CHANGED: $(echo "$RESULT" | jq -r '.data.diff.text')" # → 发送告警、写入数据库、触发webhook等 fi echo "$RESULT" | jq '.data.snapshot' > "$SNAPSHOT_FILE" else
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"previous":{"markdown":$(jq -Rsc . <<<"$PREV_MARKDOWN"),"contentHash":"$PREV_HASH"},"modes":["gitDiff"]}") STATUS=$(echo "$RESULT" | jq -r '.data.status') if [ "$STATUS" = "changed" ]; then echo "CHANGED: $(echo "$RESULT" | jq -r '.data.diff.text')" # → 发送告警、写入数据库、触发webhook等 fi echo "$RESULT" | jq '.data.snapshot' > "$SNAPSHOT_FILE" else
首次抓取 — 存储快照
curl -s -X POST "$CRW_API_URL/v1/change-tracking/diff"
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"modes":["gitDiff"]}"
| jq '.data.snapshot' > "$SNAPSHOT_FILE" fi
-H "Authorization: Bearer $CRW_API_KEY"
-H "Content-Type: application/json"
-d "{"current":{"markdown":$(jq -Rsc . <<<"$CURRENT")},"modes":["gitDiff"]}"
| jq '.data.snapshot' > "$SNAPSHOT_FILE" fi
undefinedTips
小贴士
- Persist from each result as the next call's
snapshot. Thepreviousfield in the response contains the normalized content andsnapshot— store it, don't recompute it.contentHash - means no
firstObservation: truewas supplied. The server setspreviousand returnsstatus: "changed"but produces no diff. Store it as your baseline.snapshot - mode needs
json(+ optionally a schema). Without structured input it produces no diff — usecurrent.jsonmode for plain markdown.gitDiff - Batch is more efficient at scale. One HTTP round-trip for N pages instead
of N calls. Top-level /
modesas defaults keeps the body compact.schema - Data sovereignty. You supply ; crw computes and returns. Nothing is stored server-side. Your snapshots, your infra, your retention policy.
previous
- 持久化每次结果中的,作为下一次调用的
snapshot。响应中的previous字段包含标准化内容和snapshot——直接存储即可,无需重新计算。contentHash - ****表示未传入
firstObservation: true快照。服务器会设置previous并返回status: "changed",但不会生成差异内容。将其作为基准快照存储。snapshot - 模式需要
json(可选搭配schema)。如果没有结构化输入,将无法生成差异内容——纯markdown内容请使用current.json模式。gitDiff - 批量模式在大规模场景下更高效。一次HTTP请求即可处理N个页面,而非N次请求。顶层的/
modes作为默认配置,可让请求体更简洁。schema - 数据主权。由你提供快照;crw仅负责计算并返回结果。服务器端不会存储任何数据。你的快照、你的基础设施、你的数据保留策略,完全由你掌控。
previous
See also
相关文档
- crw-scrape — get the current page content to feed into the diff
- crw — ladder overview and routing rules
- crw-scrape — 获取当前页面内容,用于差异对比
- crw — 工作流阶梯概述与路由规则