本地攝取 (Local Ingest)
在不觸發遠端提取路徑的情況下,將變更日誌上線。遠端路徑(releases admin source fetch → POST /v1/workflows/update → API 工作者的 DeterministicUpdateWorkflow, #1946)會在伺服器端執行內容主體→記錄的提取(增量 Haiku 或工具迴圈),並對該推理進行計費——即使它寫入了 0 條發布。本地的 Claude Code 代理已經是一個具備網路擷取和檔案工具能力的模型,因此它可以內聯(在您當前的工作階段內)執行每頁的提取,而只需要一個確定性的寫入路徑。
核心原則:代理就是提取器。 您負責擷取、解析、結構化記錄,並將它們 POST 到批次端點,該端點只會執行純粹的冪等向上插入(upsert),在插入時不執行任何 AI 操作。遠端提取循環永遠不會執行。
僅限本地 Claude Code。 此技能使用 Agent 工具進行子代理分支派遣,並假設存在持久的本地檔案系統以及 CLI 的 RELEASES_API_* 環境變數。它與 seeding-playbooks 並列,作為一個純本地操作員技能——不會部署到受管代理集群。
成本合約
在大規模執行之前,請向操作員明確說明並遵守以下規定:
- 花費: 代理自身用於擷取和提取的 Token,以及您派遣的任何子代理的 Token(模型由您選擇——追求品質用
sonnet,大量處理用haiku)。 - 不花費: 不會有伺服器端提取(更新工作流程的增量 Haiku/工具迴圈傳遞),不會有遠端
web_fetch提取。POST /v1/workflows/update永遠不會被呼叫。 - 批次端點在插入時不執行任何 AI 操作——不進行摘要、行銷分類、或資訊饋送增強。它執行
insert加上一個發射後不理的向量嵌入(waitUntil)。title_generated/title_short/summary等 AI 欄位會故意留空;請參閱下方的_手動增強_以了解預期的後續補救路徑。
確認未發生遠端執行的負面訊號:在 Axiom 的 releases-cloudflare-logs 中,不會有 POST .../fetch?sessionId=det-… 日誌,也不會有 extract-deps-worker 事件。
使用時機
- 在 Claude Code 中以互動方式為新資料來源上線(或回溯補齊現有資料來源),並希望跳過遠端提取成本。
- 遠端提取耗費了完整的提取循環卻寫入了 0 條發布(Conductor / 大型主體 /
max_tokens失敗模式)——內聯提取可以完全繞過該循環。 - 該頁面可以透過您自己的網路工具擷取(下方的拒絕是策略閘道,而非擷取限制)。
何時不應使用:
- 您在受管代理工作階段中。 此流程僅限本地(需要 Agent 工具分支派遣 + 本地檔案系統 + CLI 環境)。受管代理工作階段應繼續使用正常的提取路徑。
- 該資料來源有乾淨、內容豐富的資訊饋送。 直接新增它(
managing-sources),讓排程的管線去擷取。本地攝取適用於由代理內聯提取是正確工具的情況,而不是用來取代資訊饋送/GitHub 轉接器。 - 發佈商選擇退出(預檢拒絕——請參閱步驟 1)。
工作流程
步驟 1——預檢(強制安全閘道)
在擷取或寫入任何資料之前,先執行選擇退出檢查:
bun .claude/skills/local-ingest/preflight.ts <url> --json
它會擷取 robots.txt(遵循 apex→www 重新導向),解析 Cloudflare Content Signals 政策,並根據退出碼進行閘控:
| 退出碼 | 判決 | 動作 |
|---|---|---|
0 | proceed | 許可或不存在——繼續。 |
1 | refuse | 宣告了 ai-input=no。停止。向使用者說明情況。 不要擷取或寫入。 |
2 | unknown | 無法擷取/解析 robots.txt。重試一次;如果仍然未知,則向使用者說明情況——不要假設可繼續。 |
閘控僅針對 ai-input。 本地攝取是搜尋/輸入的攝取——饋送搜尋索引——這正是 ai-input 所管轄的。ai-train=no 是選擇退出模型訓練,此流程不會執行此操作,因此單獨的 ai-train=no(例如 vercel.com)會正確地繼續執行。
這是 Conductor 的教訓。 conductor.build 提供 Content-Signal: ai-train=no, search=yes, ai-input=no——其組織/資料來源早已因此原因在註冊表中暫停(releases admin playbook conductor)。本地上線絕不能違反發佈商的選擇退出而默默地花費 Token 或攝取內容。conductor.build 必須被拒絕(因其 ai-input=no)——這是此閘道的回歸測試目標。
此拒絕是策略選擇,而非技術限制:web_fetch 和 CF Browser Rendering 仍然可以擷取這些頁面(只有 Cloudflare 的 /crawl 端點對 Conductor 強制執行了此訊號)。無論如何,請尊重選擇退出。僅在操作員記錄了明確的發佈商許可時才覆寫——沒有靜默的繞過方式。
預檢還會印出 robots.txt 中的任何 Sitemap: URL——將它們保留給步驟 3 使用。
步驟 2——解析目標
確保組織和資料來源在寫入之前已存在——發布會附加到 source_id。建立/命名/主要規則請參閱 managing-sources,判斷是否存在結構化資訊饋送(若有,則偏好正常管線)請參閱 finding-changelogs。然後:
- 讀取組織操作手冊以取得解析提示:
releases admin playbook <org>(參閱managing-sources→ Playbooks)。 - 擷取已建立/解析的資料列的
sourceSlug和src_…id——批次 URL 需要其中一個。
如果資料來源是全新的,請建立它(例如 releases admin source create "<name>" --url <url> --org <org> [--primary]),但不要觸發提取——這會觸發您想要避免的遠端更新工作流程。
步驟 3——映射頁面
對頁面形狀進行分類(releases admin discovery evaluate <url> --json 會報告 pageStructure: single-page | index | unknown):
- 單頁變更日誌 → 步驟 4 中只需擷取一次。
- 索引 → 詳情頁 → 列舉每個發布的 URL。優先使用
/sitemap.xml(預檢已找到它)並過濾出變更日誌路徑;否則,解析索引 HTML 以獲取每個發布的連結。 - 客戶端渲染索引 → 索引是一個 JS 外殼,其卡片透過客戶端擷取載入,因此
curl/非 JS 爬取將返回零條目。透過渲染進行列舉:使用 Claude 在 Chrome 中驅動索引(或派遣一個渲染子代理),如果列表是分頁的則進行分頁,並收集每個發布的連結。然後檢查_詳情_頁面是否為靜態渲染的——即使索引不是,它們通常也是——如果是,則直接大量curl那些頁面(HTML→markdown 在本地執行,無需渲染,無 AI 費用)。Harvey 的help.harvey.ai/release-notes正是如此:客戶端渲染的索引,靜態的詳情頁面。不要因為索引的非 JS 爬取結果為空就得出「沒有發布」的結論。
限制在一個近期視窗內(預設約 25–50 條最新發布),並記錄您跳過了哪些內容——絕不要默默地截斷。重新執行是冪等的,因此稍後更深入的回溯補齊只會新增資料列。
步驟 4——擷取 + 提取
重複使用 parsing-changelogs 中關於類型/版本/日期/媒體的約定(請參閱_對等性_)。
- 少量頁面: 在此代理中內聯擷取和提取。
- 大量頁面: 派遣平行的子代理——每個子代理處理一個 URL 切片,每個子代理以批次模式返回記錄。使用
seeding-playbooks(以及 superpowers:dispatching-parallel-agents 以了解分支派遣規範)中的Agent工具派遣模式。不要在父代理中擷取發布 URL——將其委派出去以保持父代理的上下文乾淨。 - 父代理儲存注意事項: 子代理可能因權限限制而無法執行批次寫入。預先規劃——讓每個子代理在其結果中返回記錄,並讓父代理(您)在步驟 5 中執行寫入。這與
seeding-playbooks用於儲存筆記的模式相同。 - 提取媒體——不要只提供文字。 從每個發布主體中提取圖片/影片並填入
media,並將_next/image/_vercel/image優化器包裝展開為底層的 CDN URL(packages/rendering/src/media-url.ts中的normalizeMediaUrl)。僅傳遞文字的第一輪是常見的缺失,只有在操作員審查時才會被發現(Harvey 最初僅提供了文字,後來不得不重新提取_next/image→cdn.sanity.io並重新播種);增強後的重新 POST 使用mode: "upsert-content"(步驟 5)。
每條記錄必須符合批次模式(請參閱步驟 5)和對等規則。
可選——確定性提取(方案 A,僅限單一儲存庫): 除了手動提取,您也可以呼叫 packages/adapters/src/extract/ 中的生產級提取庫——extractFromBody、extractWithTools、runDirectFetchExtraction。ExtractDeps.cloudflare 是可為空的,repo 可以是一個空操作,因此它們只需一個 Anthropic 金鑰即可運行(證明:scripts/smoke-toolloop.ts)。這會用代理 Token 換取直接的 Anthropic API Token,但會精確地複製生產級工具迴圈。這些套件是 private: true——只能從單一儲存庫訪問,永遠無法從精簡 CLI 訪問。預設仍然是方案 B(代理即提取器)。
步驟 5——寫入(批次向上插入)
將記錄以批次方式 POST 到批次端點,每批約 50 條(保持請求大小合理;處理器會在伺服器端進行 D1 綁定限制的分塊):
# 使用組織範圍的路徑。單純的 /v1/sources/:slug 格式只接受一個有類型的
# `src_…` id——人類可讀的 slug 在那裡會返回 400 bare_slug_rejected(slug
# 是組織範圍的,#690)。使用有類型的 id 時,單純的路徑格式也能運作:
# POST $RELEASES_API_URL/v1/orgs/<orgSlug>/sources/<sourceSlug>/releases/batch
# POST $RELEASES_API_URL/v1/sources/<src_…>/releases/batch
curl -sS -X POST "$RELEASES_API_URL/v1/orgs/<orgSlug>/sources/<sourceSlug>/releases/batch" \
-H "Authorization: Bearer $RELEASES_API_KEY" \
-H "Content-Type: application/json" \
-d @chunk.json # { "releases": [ … ] }
每條發布的欄位(實際接受的形狀——workers/api/src/routes/sources.ts,postReleasesBatchHandler):
{
"version": "1.4.0", // 可選;對於以日期為首或部落格類型的資料來源,可省略或設為 null
"title": "深色模式", // 必填
"content": "Markdown 主體…", // 必填
"url": "https://…/changelog/dark-mode", // 可選但**務必填寫**——它是去重鍵
"contentHash": "…", // 可選
"publishedAt": "2026-05-30", // 可選;ISO-8601(參閱對等性)
"media": "https://…/shot.png", // 可選
"type": "feature", // 可選;"feature" | "rollup"
"prerelease": false, // 可選
}
- 冪等。 根據
UNIQUE(source_id, url)進行向上插入,填充而非覆蓋:發生衝突時,僅當儲存的行中對應欄位為_空_時,才會回填內容/媒體,因此重新執行永遠不會覆蓋好的行。可以安全地重新執行和分頁——相同的url會更新而非重複。 - 增強後的重新 POST(
mode: "upsert-content")。 由於預設是僅填充,為一個已有內容的 URL 重新 POST 更豐富的內容將是空操作——佔位內容會保留。對於有意的第二輪處理(先播種索引摘要,再補上完整詳情頁主體 + 媒體),在批次主體的頂層添加"mode": "upsert-content":它會在相同 URL 衝突時覆蓋內容/媒體,並跳過爬取標題的去重預過濾器(#1526)。僅在有意增強時使用;預設路徑保持僅填充,以便例行重新擷取不會覆蓋內容。url必須與已播種的行完全一致——增強依賴 URL 作為鍵,因此更改 URL scheme 會導致重複插入而非更新。如果 URL scheme 改變了,請重新播種:releases release delete --source <src_…> --hard然後執行新的/batch(使用預設模式)。 - 驗證。 需要
write權限;靜態的RELEASES_API_KEY(root)滿足此條件。在此儲存庫中,.env會自動載入RELEASES_API_URL+RELEASES_API_KEY。 - 插入時不執行 AI 操作(請參閱成本合約)。
分塊輔助工具(讀取 records.json 陣列,每批 50 條進行 POST):
bun -e '
const orgSlug = process.argv[1], sourceSlug = process.argv[2], file = process.argv[3];
const all = JSON.parse(await Bun.file(file).text());
const base = process.env.RELEASES_API_URL, key = process.env.RELEASES_API_KEY;
for (let i = 0; i < all.length; i += 50) {
const releases = all.slice(i, i + 50);
const res = await fetch(`${base}/v1/orgs/${orgSlug}/sources/${sourceSlug}/releases/batch`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ releases }),
});
console.log(`chunk ${i / 50}: ${res.status} ${(await res.text()).slice(0, 200)}`);
}
' <orgSlug> <sourceSlug> records.json
步驟 6——驗證
releases tail <slug> --json # 或:get_latest_releases(類型化工具)
確認發布具有非空的標題、日期、內容和媒體。如果某行是_空_的,修正提取並重新執行——預設的向上插入會安全地回填空行。如果某行已經有_薄但非空_的內容(例如您現在想用完整主體替換的索引摘要佔位內容),單純的重新執行是空操作——請使用步驟 5 中的 mode: "upsert-content" 增強重新 POST。
是在恢復一個資料來源,而非上線一個新的? 此步驟僅確認已寫入的內容——它無法說明未寫入的內容。在告訴任何人某個資料來源已「恢復」或「沒有缺失任何東西」之前,請參閱 backfilling-sources → 在宣告資料來源「乾淨」或「已恢復」之前,了解所需的即時頁面抽查。
步驟 7——操作手冊備註
記錄此資料來源的攝取方式,以便未來的代理無需重新推導。透過 manage_playbook(update_notes 動作)或 releases admin playbook <org> --notes-file - 進行。備註中應說明該資料來源是本地攝取的(尚未納入 cron 路徑)、觀察到的發佈頻率,以及任何提取特性。請遵循 managing-sources 中的操作手冊撰寫規範(使用命令語氣、保持測試、### Fetch instructions / ### Traps / ### Coverage)。
...
[內容因翻譯而截斷]