接入文档

把你的产品接入 AI Relay:选档位、写清单、实现 POST /relay、过接入验收。

L2:异步与多方案

适合当场给不了结果的动作:要排队、要人工处理、要等第三方,或者要多家报价让用户选。先读 L1:一个端点,L2 在它之上加几样东西,1–2 天能做完。

模式 清单 mode 例子 示例清单
异步单结果 async 发布一篇文章、生成一份报告 l2-publisher.json
异步多方案 async_multi 发需求收报价、找服务商,用户选一个 l2-matching.json

一单怎么走

Relay ──action.invoke──▶ 你:回 accepted(task_id)          订单:供给方已接单
你    ──回调 completed / failed──▶ Relay                    订单:已完成 / 失败
         (回调丢了:Relay 按 1、2、5、15 分钟发 task.poll 来取,之后每 15 分钟一次)
Relay ──task.cancel──▶ 你:回 cancelled 或 cancel_failed     用户取消或超时

多方案在中间多一段:

你    ──回调 proposal.add(每来一个方案发一次)──▶ Relay    订单:方案待选
         用户在 AI 里选一个,需要确认的在网页上确认
Relay ──proposal.select──▶ 你:回 completed(当场交付)或 selected(之后再回调 completed)

多方案要处理的事

多方案不只是「异步再加回调」。一个撮合、报价类的产品通常要同时处理下面 7 件事,漏一件,验收或真实用户就会卡住。L2 起步模板把它们都写好了(find_vendor 动作),本机验收 35 项全过,建议从它改起。

要处理的 规则 模板里在哪
账号绑定 用户的需求要挂到你产品的账号上:一次性绑定码 + binding.verify,见账号绑定 newBindingCode、bindingVerify
幂等接单 同一个 idempotency_key 再来回第一次的 accepted,不能再建一个需求 invoke
推送方案 每来一个方案发一次 proposal.add;task.poll 时回当前全部有效方案 advanceTasks、taskResponse
选定 核对方案属于这一单、仍然有效,否则回 failed;当场交付回 completed,之后才能交付回 selected 再回调 proposalSelect
取消 回 cancelled 之后作废所有没发出去的回调,之后补拉回 failed;重复取消回同样结果;撤不回回 cancel_failed taskCancel
回调重试 状态变化和待发回调在同一个事务里写进发件箱;5xx、超时用同一个 event_id 退避重发;401/404/409/422 不重发 enqueue、deliverOutbox
终态 completed、failed、cancelled 之后不再发改状态的回调;task.poll 一直给出同一个最终结果 taskResponse

你这边一单的状态这样走(Relay 那边的订单状态由它自己算,你不用存):

working ──方案到了──▶ proposing ──用户选定──▶ completed
   │                     │
   └──────取消───────────┴──────▶ cancelled(之后补拉回 failed)
   └──办不成──▶ failed

验收和测试时会给测试买家、测试商家下真实的需求,见测试隔离。

清单里要改的

字段 写法
mode async 或 async_multi
billing on_complete(完成时扣手续费)或 on_accept(接单时扣,只用于异步);不收费写 none
timeout_seconds 多久还没结果就由 Relay 取消,60 秒到 7 天,默认 24 小时

对外发布、替用户付款这类动作风险写 high,每次都要用户确认(见接入总览的审核标准)。

要实现的信封

type 什么时候来 你返回
action.invoke 下单 accepted(带 task_id,可带 eta_seconds);当场就失败可以直接回 failed
task.poll Relay 补拉 pending、completed、failed;多方案还没选定时回 proposals(当前全部有效方案)
task.cancel 用户取消(reason: "user_cancelled")、超时(expired)、方案没人选(no_selection) 撤回了回 cancelled;已经办完、撤不回了回 cancel_failed
proposal.select 用户选定一个方案(只有多方案) 当场交付回 completed;受理了、之后交付回 selected;方案已不可选回 failed

action.invoke 一样要按 idempotency_key 去重:同一个键再来,回第一次的 accepted,不要再建一个任务。

取消的规则:

  • 回 cancelled 之后,这一单不能再发改状态的回调(completed、failed、proposal.*)。
  • 回 cancel_failed 时订单保持原状态,用户会被告知撤不回。超时引起的会顺延一次,你可以在顺延期内交付。
  • 同一个 task.cancel 再来,回同样的结果。

回调

POST https://relay.newbeeai.online/v1/callbacks/{order_id}
X-Relay-Signature: t=…,v1=…        用同一个 secret 对原始请求体签名
事件 字段 什么时候发
completed output 任务完成;多方案在用户选定、交付之后
failed error 任务失败
proposal.add proposal 多方案:每来一个方案发一次
proposal.withdraw proposal_id、reason? 方案撤回。已选的方案被撤回时,订单回到「方案待选」,用户可以再选
cancel_failed reason? 收到 task.cancel 时已撤不回(没有在响应里直接回 cancel_failed 时用)
order.dispute reason 对已扣费的订单有异议,扣费后 72 小时内,见计费与结算
  • 每个事件带 event_id,你生成。Relay 按 (order_id, type, event_id) 去重,重复发是安全的。
  • 收到 5xx 或超时,用同一个 event_id 退避重发。进程重启后还要重发的,把 event_id 存下来。
  • Relay 回 200 表示收下(生效过的事件重发也回 200,带 duplicate: true);401 验签失败;404 订单不存在;409 事件只记录不生效(订单已是终态,或 proposal_exists:同一个方案号已经收过、内容不同,要改先撤回再用新方案号);422 事件不合 Schema、路径和请求体的 order_id 不一致、订单当前状态不接受这个事件(比如还没选定方案就发 completed,selection_required),或 proposal_mismatch:多方案单 completed 里的 output.data.proposal_id 不是用户选定的方案(不成交、不计费);413 请求体超过 1 MB。401、404、413、内容错误的 422 和终态的 409 原样重发没有用,先按错误码改正;取决于订单状态的拒绝(比如还没选定方案就发 completed,422 selection_required)不要丢,等订单状态变了用同一个 event_id 再发,或等 Relay 的 task.poll。例外是你撤回了的方案:它不会再被选定,它的 completed 要丢掉,回调和 task.poll 都别再报。只有一种情况不算:撤回时 Relay 还没记下你对选它的那次 proposal.select 的答复,而你答的是 selected / completed:以 Relay 收到的答复为准,方案恢复成已选,它的 completed 照常报(Relay 自己耽搁、这期间用户又改选了别的方案的少数情况见规范第 12 节)。
  • 被拒的事件不占去重键:同一个 event_id 重发会按订单当时的状态重新判定,条件满足后(比如用户选定了方案)可以生效。
  • 多方案单的 completed 在 output.data.proposal_id 写上方案号:写的不是用户选定的方案就不成交(proposal_mismatch);不写的,Relay 当成收到时用户选定的那个方案的结果(规范第 7 节)。
  • 没有回调能力也能接:只实现 task.poll,Relay 按 1、2、5、15 分钟补拉,用户会多等一会儿。

方案(proposal)的结构:proposal_id、title(给人看,1–200 字)、content(和 completed 的 output 一样是 data + parts),可选 price_cents(方案报价:用户选它时和用户的单笔上限比较;有成交价、按完成计费的单也按它算手续费,见计费)和 eta_days。

用 Node SDK

import { RelayCallbackClient, accepted, cancelled, cancelFailed, completed, createRelayHandler, pending, toNodeHandler } from "@dreamcavy/ai-relay-sdk";

const callbacks = new RelayCallbackClient({ secret: process.env.RELAY_SECRET! });

const relay = createRelayHandler({
  secrets: [process.env.RELAY_SECRET!],
  manifest,
  handlers: {
    publish_article: async (input, ctx) => {
      const job = await jobs.create({ relayOrderId: ctx.orderId, input });  // 存库,别只放内存
      return accepted(job.id, 600);
    },
  },
  tasks: {
    poll: async ({ task_id }) => {
      const job = await jobs.get(task_id);
      return job.done ? completed({ article_url: job.url }) : pending(300);
    },
    cancel: async ({ task_id }) => ((await jobs.tryCancel(task_id)) ? cancelled() : cancelFailed("文章已发出")),
  },
});

// 任务做完时(在你的队列或后台进程里)
await callbacks.completed(job.relayOrderId, completed({ article_url: job.url }).output);

多方案再加 tasks.select,方案用 callbacks.addProposal(orderId, proposal("p1", "方案一", { … }, { priceCents: 300000 })) 推送,撤回用 callbacks.withdrawProposal(orderId, "p1", "对方已撤回")。回调客户端会签名、生成 event_id、在 5xx 和超时时用同一个 event_id 重发,见 Node SDK README。

用 Python SDK

from ai_relay_sdk import RelayCallbackClient, RelayHandler, accepted, cancel_failed, cancelled, completed, pending

callbacks = RelayCallbackClient(secret=os.environ["RELAY_SECRET"].split(",")[0])

def publish_article(data, ctx):
    job = jobs.create(relay_order=ctx.order_id, data=data)   # 存库
    return accepted(job.id, eta_seconds=600)

def task_poll(envelope):
    job = jobs.get(envelope["task_id"])
    return completed({"article_url": job.url}) if job.done else pending(eta_seconds=300)

def task_cancel(envelope):
    return cancelled() if jobs.try_cancel(envelope["task_id"]) else cancel_failed("文章已发出")

relay = RelayHandler(
    secrets=os.environ["RELAY_SECRET"].split(","), manifest="manifest.json",
    actions={"publish_article": publish_article}, task_poll=task_poll, task_cancel=task_cancel,
)

# 任务做完时
callbacks.completed(job.relay_order, {"article_url": job.url})

多方案用 proposal_select= 处理选定,callbacks.proposal_add(order_id, proposal(...)) 推送方案。完整可运行的例子:summarizer.py。

自测

接入验收工具会跟着每个异步动作走到最终结果,再另下一单测取消。本机起一个回调接收端,把你的产品发回调的根地址临时指到它:

npx @dreamcavy/ai-relay-conformance --manifest manifest.json \
  --endpoint http://127.0.0.1:8787/relay --inputs inputs.json --callback-port 8788
# 回调接收端:http://127.0.0.1:8788/v1/callbacks/{order_id}

多方案会先用一个不存在的 proposal_id 选一次(必须回 failed),再选第一个有效方案。后台的在线验收用 Relay 的真实回调地址收验收订单,不计费。检查项和参数见 Conformance README。

完整的状态和规则见规范第 7 节(回调)和第 12 节(订单与计费)。