Community写作与编辑github.com

buildinternet/releases

产品发布说明注册表,专为AI代理设计。通过MCP+CLI获取最新的产品更新日志。

releases 是什么?

该技能为Claude Code等AI代理提供了一种本地化的产品更新日志摄入机制,旨在替代远程服务器端提取流程,从而降低API调用成本和推理开销。其核心思想是将代理作为提取器,让代理在本地会话中完成网页抓取、内容解析和结构化,然后通过批量端点进行幂等写入。这适用于新源接入、远程提取失败或需要跳过服务器端AI提取的场景,但仅限于本地Claude Code环境,不适用于托管代理会话。技能要求操作者明确成本契约:仅消耗本地代理的token,不会产生任何服务器端AI提取费用。同时,它强制进行预飞行检查(robots.txt/`ai-input`信号)以尊重发布者的退出政策,并提供了完整的七步工作流,包括预飞行、源解析、页面映射、并行提取、批量写入、结果验证和Playbook记录。该技能还提供了可选的生产级确定性提取路径,通过调用monorepo中的提取库来精确复现生产环境的工具循环。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add buildinternet/releases

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

本地摄入

在不触发远程提取路径的情况下接入更新日志。远程路径(releases admin source fetch → POST /v1/workflows/update → API worker的 DeterministicUpdateWorkflow, #1946)在服务端执行正文→记录提取(增量Haiku或工具循环),即使写入零条发布也要为推理付费。而本地Claude Code代理本身就是具备网络抓取和文件工具的强大模型,因此可以在当前会话内内联执行逐页提取,仅需一个确定性的写入路径。

核心原则:代理即提取器。 你负责抓取、解析、结构化记录,然后通过批量端点进行POST,该端点在插入时仅执行纯幂等更新 ,无需任何AI。远程提取循环永远不会运行。

仅限本地Claude Code。 该技能使用Agent工具进行子代理扇出,并假设存在持久化的本地文件系统和CLI的RELEASES_API_*环境变量。它作为仅限本地的操作者技能,与seeding-playbooks并列——不会部署到托管代理集群。

成本契约

在大型运行前向操作者明确说明并遵守:

  • 消耗: 你的代理自身用于抓取和提取的token,以及你扇出的任何子代理的token(模型由你选择——质量用 sonnet,批量用 haiku)。
  • 不消耗: 无服务端提取(更新工作流的增量Haiku/工具循环流程),无远程 web_fetch 提取。绝不调用 POST /v1/workflows/update。
  • 批量端点在插入时不运行任何AI——无摘要生成、无营销分类器、无feed丰富化。它只执行 insert 加上一个即发即忘的向量嵌入(waitUntil)。AI字段(title_generated / title_short / summary)有意留空;参见下方的 手动丰富化 了解后续的刻意处理路径。

确认未发生远程运行的负面信号:在Axiom的 releases-cloudflare-logs 中,没有 POST .../fetch?sessionId=det-…,也没有针对该源的 extract-deps-worker 事件。

何时使用

  • 在Claude Code中交互式接入新源(或回填现有源),并且希望跳过远程提取成本。
  • 远程抓取消耗了整个提取循环但写出了0条发布(Conductor/大正文/max_tokens失败模式)——内联提取完全绕过了这个循环。
  • 页面可以通过你自己的网络工具抓取(下面的拒绝是策略关口,而非抓取限制)。

何时不使用:

  • 你在托管代理会话中。 此技能仅限本地(Agent工具扇出 + 本地文件系统 + CLI环境)。托管代理会话应继续使用正常的抓取路径。
  • 源具备干净、内容丰富的feed。 直接添加它(managing-sources),让cron流水线抓取即可。本地摄入适用于代理内联提取是正确工具的场景,而不是feed/GitHub适配器的替代品。
  • 发布者选择退出(预检拒绝——参见步骤1)。

工作流

步骤 1 — 预检(强制安全门控)

在抓取或写入任何内容之前运行退出检查:

bun .claude/skills/local-ingest/preflight.ts <url> --json

它会抓取 robots.txt(遵循顶级域→www重定向),解析Cloudflare Content Signals 策略,并根据退出码进行门控:

退出码判定行动
0proceed允许或缺失——继续。
1refuse声明了 ai-input=no。停止。向用户呈现。 不要抓取或写入。
2unknown无法抓取/解析 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 — 解析目标

在写入之前确保组织(org)和源(source)存在——发布记录附加到 source_id。关于创建/命名/主要规则的详细信息,请参考 managing-sources;关于是否存在结构化feed(如果存在,优先使用正常流水线)的详细信息,请参考 finding-changelogs。然后:

  • 阅读组织playbook获取解析提示: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抓取返回零条目。通过渲染来枚举:在Chrome中使用Claude驱动索引(或扇出一个渲染子代理),如果列表是分页的则进行翻页,收集每个发布的链接。然后检查_详情_页面是否为静态渲染——即使索引不是,详情页面通常也是静态渲染的——如果是,直接批量 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(normalizeMediaUrl 来自 packages/rendering/src/media-url.ts)。纯文本第一遍是常见失误,仅在操作者审查时才会被发现(Harvey提交了纯文本,然后不得不重新提取 _next/image → cdn.sanity.io 并重新播种);丰富化重新POST使用 mode: "upsert-content"(步骤5)。

每条记录必须符合批量模式(参见步骤5)和一致性规则。

可选——确定性提取(方法A,仅限monorepo): 除了手动提取,你可以调用 packages/adapters/src/extract/ 中的生产提取库——extractFromBody、extractWithTools、runDirectFetchExtraction。ExtractDeps.cloudflare 可为null,repo 可以是noop,因此它们仅凭Anthropic密钥即可运行(证明:scripts/smoke-toolloop.ts)。这用代理token换取直接的Anthropic API token,但精确复现了生产工具循环。这些包是 private: true ——仅可从monorepo访问,永远不能从精简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方案会插入重复项。如果URL方案已更改,改为重新播种:releases release delete --source <src_…> --hard 然后新的 /batch(默认模式)。
  • 认证。 需要 write 作用域;静态 RELEASES_API_KEY(root)满足此要求。在此仓库中,.env 自动加载 RELEASES_API_URL + RELEASES_API_KEY。
  • 插入时无AI(参见成本契约)。

分块帮助脚本(读取 records.json 数组,以每批50条发布):

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 — Playbook记录

记录此源是如何被摄入的,以便未来的代理无需重新推导。通过 manage_playbook(动作 update_notes)或 releases admin playbook <org> --notes-file - 完成。注明该源是本地摄入的(尚未在cron路径上)、其观察到的发布节奏以及任何提取中的注意事项。遵循 managing-sources 中playbook编写规范(祈使语气、保持测试、### 抓取说明 / ### 陷阱 / ### 覆盖范围)。请勿

…

[已截断以进行翻译]

相关技能