--- 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`,见下方重试小节的超时数字),例如: ```bash 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`,不能因为"技术上收到了响应"就跳过求助。 具体调用: ```bash node ../../../../../bin/lemon.js human request --from "pricing-api-integrator" \ --type api_key --key "<变量名>" --reason "<按上面两种文案二选一,只传变量名不传猜测的密钥值>" ``` ## 价格快照输出格式 ```json { "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` 更省事。