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 都不写死这些路径,因为它们随任务变化:
- [上游输入] —— 采价目标清单(竞品名称 / 产品 SKU / API 端点或文档链接)在哪,逐条读。标 ⚠️ 的表示还没产出。
- [写入边界] —— 价格快照只能写进这个目录,越界判失败。
- [节点产物目录] ——
_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——这条零容忍,一例违反即整条快照不可信。
判定顺序(按此顺序逐条走,不要跳步):
- 请求都没发出去(比如目标清单里这一条压根没有可用的 API 端点信息)—— 这不是"请求失败",是 RESEARCH 阶段就该识别的输入缺陷,PLAN 阶段应跳过该目标并在
_plan.md里写明原因,不进入 EXECUTE。 - 鉴权环境变量为空(
echo $VAR无输出)—— 走「求助流程」,status: "blocked_on_credential",reason: "credential_missing"。 - 请求发出,收到 401/403(变量存在但被拒)—— 同样走「求助流程」,
status: "blocked_on_credential",reason: "credential_invalid"。不要判成unavailable:无效凭证换个时间点重试也没用,必须人工介入换 Key,语义上等同"缺凭证",只是缺失 vs 已配但失效的区别,用reason区分即可,不要用status区分。 - 请求发出,收到 400/404(参数错误 / 目标资源不存在,如 SKU 在该竞品站点确实没有)—— 直接
status: "unavailable",reason: "not_found"(404)或"invalid_request"(400)。不重试,不占重试预算。 - 请求发出,收到 200,但 body 是空结果(如批量查价接口返回的字典里根本没有这个 id 的 key,或数组为空)—— 这不是
ok(没有价格可报),也不是"该重试"的失败(接口如实告诉你没这条数据,重试还是没有)。判status: "unavailable",reason: "not_found",与场景 4 共用同一个 reason 值(本质上都是"目标不存在于对方数据里"),但http_status字段照实填 200,供 VERIFY/下游区分"接口层面就没找到"和"请求本身失败"。 - 请求发出,收到 429——进入「重试/退避策略」的 429 分支;重试预算耗尽仍 429,
status: "unavailable",reason: "rate_limited"。 - 请求发出,收到 500/502/503/504——进入「重试/退避策略」的 5xx 分支;重试预算耗尽仍失败,
status: "unavailable",reason: "server_error"。 - 连接/读取超时——进入「重试/退避策略」的超时分支;重试预算耗尽仍超时,
status: "unavailable",reason: "timeout"。 - 单目标总耗时超过 45s deadline(跨多次重试累计)——立即终止该目标的重试,
status: "unavailable",reason: "retry_budget_exhausted"。 - 请求发出,收到 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-Afterheader(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 封顶(本体的重试次数上限本身就比封顶所需的轮数低,封顶条款是防御性上限,不是常规路径会走到的分支)。
求助流程(三条裁决,必须按此实现)
- 去重粒度:按环境变量名去重。EXECUTE 阶段开始前,先扫一遍本次目标清单,把所有需要鉴权的目标按"所需环境变量名"分组;每组只在第一次遇到该变量缺失/无效时调用一次
human request,同组其它目标直接复用同一次判定结果(同样标blocked_on_credential),不重复求助。 - 求助后不阻塞、不回头轮询:
human request调用后立即把当前目标标blocked_on_credential并继续处理清单里下一个目标。同一次 EXECUTE 内不再回头检查这把 Key 是否已被人类配上——即使后续目标恰好用到同一变量名,也按本次 EXECUTE 开始时的探测结果为准,不做运行中动态刷新。这把 Key 配没配上,留到下一轮任务重新探测。 - 缺失 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更省事。