crw-watch

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

crw-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 cron
crw不会在调用之间存储任何数据,调用方拥有快照的所有权:
1. 立即抓取页面        → 存储快照(markdown / json格式)
2. 稍后再次抓取      → 调用/v1/change-tracking/diff接口,传入当前快照与历史快照
3. 当status=changed时 → 触发告警/执行操作
4. 通过cron重复执行上述步骤

Diff modes

差异对比模式

Two modes, composable:
ModeWire stringWhat it produces
Git-style text diff
"gitDiff"
(alias:
"git-diff"
)
Unified-diff text + parse-diff AST in
diff.text
/
diff.json
Per-field JSON diff
"json"
Path-keyed map
{"$.field": {"previous":…,"current":…}}
in
diff.json
; requires
schema
Default (omit
modes
):
["gitDiff"]
. Combine both:
"modes": ["gitDiff", "json"]
.
两种可组合的模式:
模式标识字符串输出内容
Git风格文本差异
"gitDiff"
(别名:
"git-diff"
统一差异文本 + 可解析的diff抽象语法树,分别在
diff.text
/
diff.json
按字段JSON差异
"json"
路径键映射
{"$.field": {"previous":…,"current":…}}
,位于
diff.json
中;需要传入
schema
默认模式(省略
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
batch
key):
bash
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
modes
/
schema
/
prompt
/
contentType
at the top level are defaults; each batch item can override them individually.
Inline during a scrape — pass
changeTracking
as a format on
/v1/scrape
:
bash
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": [...] }
    }
  }
}
批量差异对比(通过
batch
字段区分):
bash
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"]
  }'
顶层的
modes
/
schema
/
prompt
/
contentType
为默认配置;每个批量任务项可单独覆盖这些配置。
抓取时内嵌对比 — 在调用
/v1/scrape
时传入
changeTracking
作为格式参数:
bash
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:
{ batch: [...items], modes, schema?, ... }
where each item is
{ url?, current, previous?, modes?, schema?, ... }
.
FieldTypeNotes
current.markdown
stringCurrent page content (gitDiff / mixed)
current.json
objectCurrent extracted JSON (json / mixed)
previous.markdown
stringPrior snapshot for gitDiff
previous.contentHash
stringPersist from prior result's
snapshot.contentHash
modes
string[]
["gitDiff"]
(default),
["json"]
, or both
schema
JSON SchemaRequired for
json
mode; defines tracked fields
prompt
stringNatural-language extraction prompt (alternative to
schema
)
contentType
stringIf binary/non-text, triggers byte-hash comparison only
tag
stringOpaque caller ID echoed back on the result
goal
stringNatural-language filter for meaningful changes (AI judge, M2)
judgeEnabled
boolEnable 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?, ... }
字段类型说明
current.markdown
字符串当前页面内容(适用于gitDiff / 混合模式)
current.json
对象当前提取的JSON数据(适用于json / 混合模式)
previous.markdown
字符串gitDiff模式所需的历史快照
previous.contentHash
字符串从之前请求结果的
snapshot.contentHash
中获取并持久化
modes
字符串数组默认值
["gitDiff"]
,可选
["json"]
,或同时启用两种模式
schema
JSON Schemajson模式必填;用于定义需追踪的字段
prompt
字符串自然语言提取提示词(作为
schema
的替代方案)
contentType
字符串如果是二进制/非文本内容,仅触发字节哈希对比
tag
字符串不透明的调用方ID,将在结果中原样返回
goal
字符串用于定义“有意义变更”的自然语言过滤器(AI判断功能,M2版本)
judgeEnabled
布尔值启用AI判断功能(M2版本特性;当前已支持参数传入但暂未生效)

The
goal
field (AI judge)

goal
字段(AI判断)

goal
is a natural-language filter for what counts as a meaningful change, fed to an LLM judge. It is accepted by the server now but applied in a future milestone (M2). Guidance for when it lands:
  • Be specific:
    "Alert when the listed price changes; ignore copy rewrites and nav updates"
    beats
    "detect important changes"
    .
  • Narrow the scope:
    "Only flag changes to the Features table, not the hero section"
    .
  • The judge returns
    {meaningful, confidence, reason, meaningfulChanges[]}
    in the result's
    judgment
    field.
goal
是用于定义“有意义变更”的自然语言过滤器,将输入给LLM判断模型。当前服务器已支持该参数,但功能将在后续里程碑(M2版本)中生效。功能上线后的使用指南:
  • 描述要具体:
    "当列出的价格变更时触发告警;忽略文案改写和导航更新"
    "检测重要变更"
    效果更好。
  • 缩小范围:
    "仅标记Features表格的变更,忽略hero区域的更新"
  • 判断结果将在返回数据的
    judgment
    字段中包含
    {meaningful, confidence, reason, meaningfulChanges[]}

Cron pattern (self-hosted)

Cron定时脚本(自部署场景)

bash
#!/usr/bin/env bash
bash
#!/usr/bin/env bash

cron-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

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
undefined
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')" # → 发送告警、写入数据库、触发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
undefined

Tips

小贴士

  • Persist
    snapshot
    from each result
    as the next call's
    previous
    . The
    snapshot
    field in the response contains the normalized content and
    contentHash
    — store it, don't recompute it.
  • firstObservation: true
    means no
    previous
    was supplied. The server sets
    status: "changed"
    and returns
    snapshot
    but produces no diff. Store it as your baseline.
  • json
    mode needs
    current.json
    (+ optionally a schema).
    Without structured input it produces no diff — use
    gitDiff
    mode for plain markdown.
  • Batch is more efficient at scale. One HTTP round-trip for N pages instead of N calls. Top-level
    modes
    /
    schema
    as defaults keeps the body compact.
  • Data sovereignty. You supply
    previous
    ; crw computes and returns. Nothing is stored server-side. Your snapshots, your infra, your retention policy.
  • 持久化每次结果中的
    snapshot
    ,作为下一次调用的
    previous
    。响应中的
    snapshot
    字段包含标准化内容和
    contentHash
    ——直接存储即可,无需重新计算。
  • **
    firstObservation: true
    **表示未传入
    previous
    快照。服务器会设置
    status: "changed"
    并返回
    snapshot
    ,但不会生成差异内容。将其作为基准快照存储。
  • json
    模式需要
    current.json
    (可选搭配schema)
    。如果没有结构化输入,将无法生成差异内容——纯markdown内容请使用
    gitDiff
    模式。
  • 批量模式在大规模场景下更高效。一次HTTP请求即可处理N个页面,而非N次请求。顶层的
    modes
    /
    schema
    作为默认配置,可让请求体更简洁。
  • 数据主权。由你提供
    previous
    快照;crw仅负责计算并返回结果。服务器端不会存储任何数据。你的快照、你的基础设施、你的数据保留策略,完全由你掌控。

See also

相关文档

  • crw-scrape — get the current page content to feed into the diff
  • crw — ladder overview and routing rules
  • crw-scrape — 获取当前页面内容,用于差异对比
  • crw — 工作流阶梯概述与路由规则