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,422selection_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 节(订单与计费)。