SKILL.md 17.2 KB

name: pricing-api-integration-loop description: "定价 API 对接节点操作手册:读上游采价目标 → 调用竞品公开定价 API → 只认实时响应、拒绝估算/缓存数据 → 交付结构化价格快照+log 的四段式细则,含缺鉴权求助流程、重试退避基线、状态分类规则。" layer: 2 domain: data-integration keywords:

  • pricing-api
  • live-price
  • no-estimate
  • human-request
  • research-plan-execute-verify

- retry-backoff

Skill: 定价 API 对接节点操作手册

何时用

每次收到「抓取竞品实时定价」类任务时,无论目标清单有几条,四段式都不跳。

开工前先定位三类路径(每次任务都做,不要复用上次记忆)

任务描述里有三段绝对路径,本文件和 agent.md 都不写死这些路径,因为它们随任务变化:

  1. [上游输入] —— 采价目标清单(竞品名称 / 产品 SKU / API 端点或文档链接)在哪,逐条读。标 ⚠️ 的表示还没产出。
  2. [写入边界] —— 价格快照只能写进这个目录,越界判失败。
  3. [节点产物目录] —— _research.md / _plan.md / _lessons.md / pricing-api-integrator.log 落在这里。

两处产出、两个目录,不许混放:snapshot(价格快照)只写进「写入边界」,不许连带写进「节点产物目录」;_research.md/_plan.md/_lessons.md/pricing-api-integrator.log 这四个阶段性/日志产物只写进「节点产物目录」,不许写进「写入边界」(下游 graph_files 只索引「写入边界」里的 artifact 和「节点产物目录」里的 log 两处,其它位置下游读不到,等于没交付)。EXECUTE 开始前把这两个绝对路径抄到 _plan.md 顶部,全程照抄不重新猜测。

核心红线:只认实时响应

  • 每一条价格数据必须来自本次任务里真实发出并收到响应的一次 API 请求。
  • 严禁:估算价、上一轮快照里的旧值、任何形式的"历史均价"或"预测价"顶替当前价。
  • 抓不到就在快照里把该条标 "status": "unavailable" 并按下方「状态分类规则」写明 reason, 不要省略这个字段去让快照看起来"每条都有数字"。

工具边界:没有专用抓取工具

../../config.json 的 cli.toolPolicy 只开放 bash / read_file / str_replace。对外请求一律用 bash 里的 curl(必须带 -w '%{http_code}' 和明确的 --connect-timeout / --max-time,见下方重试小节的超时数字),例如:

curl -sS -w '\n%{http_code}' --connect-timeout 5 --max-time 20 \
  -H "Authorization: Bearer $VENDOR_API_KEY" \
  "https://api.example-competitor.com/v1/price?sku=ABC123"

把状态码、原始响应体(完整 body,不要截断/摘要)都记进日志,不要只记"成功"或"失败"这种结论性描述——VERIFY 阶段的转述核对(见下)依赖日志里有原始数值可比对。

日志安全红线:日志里只能出现请求用了哪个环境变量名鉴权(如"本次请求使用 $VENDOR_API_KEY 鉴权"),绝不能出现该变量展开后的实际值——不要把 curl 实际执行的完整命令行(含展开后的 Authorization header 值)贴进日志,只贴脱敏后的命令模板(用 $VAR 占位)+ 响应状态码 + 响应 body。

状态分类规则(status 三态 + reason 枚举)

status 严格三选一:ok / blocked_on_credential / unavailable。status != "ok" 的条目 price 必须为 null,status == "ok" 的条目 price.amount 必须是非空 number——这条零容忍,一例违反即整条快照不可信。

判定顺序(按此顺序逐条走,不要跳步):

  1. 请求都没发出去(比如目标清单里这一条压根没有可用的 API 端点信息)—— 这不是"请求失败",是 RESEARCH 阶段就该识别的输入缺陷,PLAN 阶段应跳过该目标并在 _plan.md 里写明原因,不进入 EXECUTE。
  2. 鉴权环境变量为空(echo $VAR 无输出)—— 走「求助流程」,status: "blocked_on_credential",reason: "credential_missing"。
  3. 请求发出,收到 401/403(变量存在但被拒)—— 同样走「求助流程」,status: "blocked_on_credential",reason: "credential_invalid"。不要判成 unavailable:无效凭证换个时间点重试也没用,必须人工介入换 Key,语义上等同"缺凭证",只是缺失 vs 已配但失效的区别,用 reason 区分即可,不要用 status 区分。
  4. 请求发出,收到 400/404(参数错误 / 目标资源不存在,如 SKU 在该竞品站点确实没有)—— 直接 status: "unavailable",reason: "not_found"(404)或 "invalid_request"(400)。不重试,不占重试预算。
  5. 请求发出,收到 200,但 body 是空结果(如批量查价接口返回的字典里根本没有这个 id 的 key,或数组为空)—— 这不是 ok(没有价格可报),也不是"该重试"的失败(接口如实告诉你没这条数据,重试还是没有)。判 status: "unavailable",reason: "not_found",与场景 4 共用同一个 reason 值(本质上都是"目标不存在于对方数据里"),但 http_status 字段照实填 200,供 VERIFY/下游区分"接口层面就没找到"和"请求本身失败"。
  6. 请求发出,收到 429——进入「重试/退避策略」的 429 分支;重试预算耗尽仍 429,status: "unavailable",reason: "rate_limited"。
  7. 请求发出,收到 500/502/503/504——进入「重试/退避策略」的 5xx 分支;重试预算耗尽仍失败,status: "unavailable",reason: "server_error"。
  8. 连接/读取超时——进入「重试/退避策略」的超时分支;重试预算耗尽仍超时,status: "unavailable",reason: "timeout"。
  9. 单目标总耗时超过 45s deadline(跨多次重试累计)——立即终止该目标的重试,status: "unavailable",reason: "retry_budget_exhausted"。
  10. 请求发出,收到 200 且有正常价格数据——status: "ok",price.amount 填数值型(非字符串),http_status: 200。

unavailable 的 reason 枚举(供下游判断"该不该重试"): | reason | 语义 | 下次任务该不该重试 | |---|---|---| | not_found | 目标不存在(404 或 200+空结果) | 不该,除非上游清单改了目标 | | invalid_request | 参数错误(400) | 不该,除非 PLAN 阶段改参数 | | rate_limited | 限流重试预算耗尽 | 该,下次任务大概率能成 | | timeout | 超时重试预算耗尽 | 该,下次任务大概率能成 | | server_error | 5xx 重试预算耗尽 | 该,下次任务大概率能成 | | retry_budget_exhausted | 单目标 45s deadline 触发 | 该,但要留意该目标是否系统性变慢 |

重试/退避策略(Digest 阶段裁定的正式基线,不是"合理即可")

错误类型 是否重试 base 上限 最大重试次数 备注
429(限流) 是 2s 60s 5 次 指数退避(2s→4s→8s→16s→32s,超 60s 封顶);响应带 Retry-After header 时优先按该值等待,忽略指数计算值
500/502/503/504 是 2s 30s 3 次 指数退避(2s→4s→8s,超 30s 封顶)
网络超时 是 2s 30s 3 次 connect timeout 5s / read timeout 20s 分开设置;超时本身触发时按此表重试,不额外区分 connect/read 的退避参数
401/403 否 — — 0 次 不进入重试循环,直接走求助流程(见「状态分类规则」步骤 3)
400/404 否 — — 0 次 不进入重试循环,直接判 unavailable(见「状态分类规则」步骤 4)
单目标 deadline — — 45s — 跨所有重试累计耗时,超过则终止该目标,不管当时在哪一类错误的重试循环里

实现要点:

  • 每次请求前检查该目标累计耗时是否已超 45s,超了直接标 retry_budget_exhausted,不再发起新请求。
  • 429 响应必须先看有没有 Retry-After header(curl -D - 或等价方式取响应头);有则 sleep <该值>;没有才用指数退避表里的数字算。
  • curl 调用必须显式传 --connect-timeout 5 --max-time 20(网络超时分支的两个数字),不要用 curl 默认值。
  • 指数退避的精确公式(消除"指数退避"这四个字的实现歧义):第 n 次重试(n 从 1 开始)等待 wait = min(base * 2^(n-1), 上限) 秒。以 429 为例(base=2s, 上限=60s):n=1→2s,n=2→4s,n=3→8s,n=4→16s,n=5→32s,全部未超 60s 封顶所以不触发封顶;5xx/超时(base=2s, 上限=30s):n=1→2s,n=2→4s,n=3→8s,第 3 次即达到最大重试次数,同样未触发 30s 封顶(本体的重试次数上限本身就比封顶所需的轮数低,封顶条款是防御性上限,不是常规路径会走到的分支)。

求助流程(三条裁决,必须按此实现)

  1. 去重粒度:按环境变量名去重。EXECUTE 阶段开始前,先扫一遍本次目标清单,把所有需要鉴权的目标按"所需环境变量名"分组;每组只在第一次遇到该变量缺失/无效时调用一次 human request,同组其它目标直接复用同一次判定结果(同样标 blocked_on_credential),不重复求助。
  2. 求助后不阻塞、不回头轮询:human request 调用后立即把当前目标标 blocked_on_credential 并继续处理清单里下一个目标。同一次 EXECUTE 内不再回头检查这把 Key 是否已被人类配上——即使后续目标恰好用到同一变量名,也按本次 EXECUTE 开始时的探测结果为准,不做运行中动态刷新。这把 Key 配没配上,留到下一轮任务重新探测。
  3. 缺失 vs 无效路径区分:两者都归 blocked_on_credential,reason 区分:
    • 环境变量为空(echo $VAR 无输出)→ reason: "credential_missing" → human request 文案:"缺少 "
    • 环境变量存在但请求返回 401/403 → reason: "credential_invalid" → human request 文案:" 已失效或权限不足,需更新" 两种情况都必须调用 human request,不能因为"技术上收到了响应"就跳过求助。

具体调用:

node ../../../../../bin/lemon.js human request --from "pricing-api-integrator" \
  --type api_key --key "<变量名>" --reason "<按上面两种文案二选一,只传变量名不传猜测的密钥值>"

价格快照输出格式

{
  "snapshot_id": "",
  "generated_at": "2026-08-24T00:00:00Z",
  "items": [
    {
      "competitor": "",
      "product_id": "",
      "api_endpoint": "",
      "status": "ok",
      "price": { "amount": 0, "currency": "USD", "unit": "" },
      "fetched_at": "2026-08-24T00:00:00Z",
      "http_status": 200,
      "source_type": "live_api"
    },
    {
      "competitor": "",
      "product_id": "",
      "api_endpoint": "",
      "status": "blocked_on_credential",
      "reason": "credential_missing",
      "price": null
    },
    {
      "competitor": "",
      "product_id": "",
      "api_endpoint": "",
      "status": "unavailable",
      "reason": "not_found",
      "price": null,
      "http_status": 200,
      "fetched_at": "2026-08-24T00:00:00Z"
    }
  ]
}

字段类型约束(数据完整性维度):

  • price.amount:number,不允许字符串("29.99" 不合规,29.99 才合规)。实测踩坑提醒:不少定价 API 把价格包在 JSON 字符串里返回(如 {"amount":"77385.565"}),直接把这个字符串抄进快照会违反类型约束——写入快照前必须显式转成 number(Number(...)/parseFloat),并在转换后核对数值没被截断或变成 NaN。
  • generated_at / fetched_at:标准 ISO8601(YYYY-MM-DDTHH:mm:ssZ)。
  • source_type:恒为 "live_api",不允许出现其它值(本体禁止换源,见硬规矩)。
  • reason:blocked_on_credential 用 credential_missing/credential_invalid;unavailable 用「状态分类规则」表里的六个枚举值之一,不允许自由文本。

四段式细则

RESEARCH

  • 逐条读上游采价目标清单,不要只扫标题;有 ⚠️ 缺失的输入立即判断能不能开工,不能就汇报 blocked。
  • 对每个目标确认:API 端点是否可达、需要哪些请求参数、要不要鉴权、鉴权用的环境变量名叫什么。
  • 检查所需环境变量是否已存在(先用 echo $VAR 探测,不要现在就发 human request——留到 EXECUTE 阶段真正要用时再求助)。
  • 本阶段不发起任何真实 curl 请求(连探测性请求也不行,端点可达性只能靠读文档/清单里给的信息判断,或留到 PLAN/EXECUTE 阶段验证)。
  • 按上面「求助流程」裁决 1,先把目标清单按所需环境变量名分组,记进 _research.md,供 PLAN 阶段直接引用分组结果。
  • 写 _research.md:目标清单逐条现状(带出处路径)+ 缺鉴权/缺端点的处置方式 + 环境变量分组表。
  • 流转:node ../../../../../bin/lemon.js task transition --agentId <你的ID> --phase plan --artifact <_research.md路径>

PLAN

  • 只凭 _research.md。逐目标列出:调用哪个端点、什么参数、按「状态分类规则」10 步判定顺序会落在哪一步。
  • 每一步必须能独立验证——"抓一下定价"这种步骤要拆到"对目标 X 发一次 curl 请求并记录状态码"的粒度。
  • 写 _plan.md,流转: node ../../../../../bin/lemon.js task transition --agentId <你的ID> --phase execute --artifact <_plan.md路径>

EXECUTE

  • 只凭 _plan.md。这是唯一允许真正发起 API 请求的阶段。
  • 逐目标推进:发请求(带 --connect-timeout 5 --max-time 20)→ 记录 http_status + 完整原始响应体到日志 → 按「状态分类规则」10 步判定顺序分类 → 填进快照。
  • 遇到缺 Key 或 Key 无效:走「求助流程」三条裁决,标 blocked_on_credential,继续下一个目标。
  • 遇到 429/5xx/超时:按「重试/退避策略」表执行,重试耗尽仍失败按对应 reason 标 unavailable。
  • 计划外发现的问题(比如清单里的端点写错了)记下来,不顺手换数据源——顺手换会让 VERIFY 阶段说不清这条数据到底信不信得过。
  • 完成后流转:node ../../../../../bin/lemon.js task transition --agentId <你的ID> --phase verify

VERIFY

  • 逐条跑 acceptance 命令,把真实终端输出(不是复述)贴进 pricing-api-integrator.log。
  • 日志-快照交叉验证:每条 status: "ok" 的记录,能不能在日志里找到 endpoint + http_status 一一对应的一次真实请求?找不到对应记录的条目一律删除或改判 unavailable,不许留在最终快照里。
  • 转述准确性核对(反幻觉,不能只查"有没有调用"):对每条 status: "ok" 的记录,把日志里记录的原始响应体数值(如 CoinGecko 返回的 usd 字段、Coinbase 返回的 amount 字段)与快照里最终的 price.amount / price.currency 逐字段比对,必须完全一致(数值不能抄错、单位不能漏、不能四舍五入)。发现不一致必须改正快照或改判 unavailable,不能带着转述误差交付。
  • 反向检查(必须是一步可执行动作,不是抽象原则):从上游目标清单之外,挑一个从未在本次 EXECUTE 里真实调用过的竞品/端点(比如清单没让抓的另一个 SKU),显式在 VERIFY 阶段核对:快照里搜不到这个目标的任何记录,尤其不能出现它的 status: "ok"。把这一步的核对结果("已确认快照中不存在 XXX 的编造记录")写进日志,不能只在脑子里想过。如果某个 API 从没被真正调用过,这条 acceptance 会红吗?红不了说明验收写空了,回 PLAN 补。
  • 全部通过 → report_task({status:"PASS"}),附 artifact 路径 + 成功抓取条数 + blocked/unavailable 条数(按 reason 细分)。
  • 任意一条 fail → 原因写进 _lessons.md,打回上一阶段自己修,不要带着已知 fail 交付。

硬规矩(违反即事故)

  • 未走完四段直接发请求或汇报 PASS。
  • 用估算价/历史价/缓存价填充快照,冒充实时数据——比交付延迟更严重的违规。
  • 交付物(快照)写到写入边界之外,或私有产物写到节点私有目录之外——两头都不报错,但下游读不到。
  • 缺鉴权时跳过目标不说,或猜一个 Key 去试——两者都会制造假成功。
  • 同一环境变量名在一次任务里重复求助超过一次——违反去重裁决,制造噪音。
  • 求助后阻塞等待人类响应,拖慢清单里其它目标的处理——违反"不阻塞"裁决。
  • 401/403 判成 unavailable(应该是 blocked_on_credential + reason: credential_invalid)——这是把"该人工介入"误判成"该重试",会让下游误以为换个时间点重试有用。
  • status == "ok" 但 price 为 null,或 status != "ok" 但 price 非 null——未定义组合,零容忍。
  • price.amount 存成字符串而非 number——下游解析会出问题。
  • 汇报里写"接口应该是通的"——没跑过就是没做完,如实报 FAIL 或 unavailable 更省事。