先接通独立身份与所需业务能力,再发布 Funnel 和固定引用。Funnel 用于编排;跨天等待与后台执行需要相应运行时,不能从图节点推定支持。
Funnel 接入与恢复
显式启动固定版本流程,处理身份、重试和结果。
SDK 示例对应当前 API。核对匹配版本与 HTTP 接入方式
API v1更新于
开始前准备
设计并显式启动
Funnel composes independent capabilities into a versioned flow. It owns graph execution and accepted results; Offering, Paywall, Coupon, commerce and GrowthBook retain their own definitions and business facts.
- 在 Funnels 创建流程,连接 Page/Popup、条件、实验与已支持的能力节点。引用已发布 Paywall 时固定其 Version;发布保存当前图和资源引用。
- 从你自己的 SDK/API 调用显式启动。匹配版本的后端可用 campaign_decide 固定活动决策、campaign_start 按 shown 记录实施频控;普通 decide/start 不自动启用。先验证宿主登录,再签发当前 Customer 会话。访客先创建 Guest 并取得 Guest 会话。
- 记录原 requestId 和整个 start 输入,响应丢失时原样重试。SDK 安装要求见本页版本提示。
import { createFunnelStartClient, createIndianaServer, createGuestToken,} from "@tinkerlandai/indiana-sdk/server"; const options = { baseUrl: process.env.INDIANA_BASE_URL!, apiKey: process.env.INDIANA_SECRET_KEY!, scope: { projectId: process.env.INDIANA_PROJECT_ID!, environment: "sandbox" },} as const;const indiana = createIndianaServer(options);const funnels = createFunnelStartClient(options); export async function POST(request: Request) { if (request.headers.get("origin") !== process.env.APP_ORIGIN) return new Response("Forbidden", { status: 403 }); const session = await requireHostSession(request); // Every run has a Customer. A signed-in user is named by your own user ID: Indiana creates the // Customer on first use with creation-only customerData (needs customers:write). A visitor is a // Guest kept in this session, referenced by its Indiana Customer ID. let customer; if (session.userId) { customer = { type: "externalUserId", value: session.userId } as const; } else { session.indianaGuestToken ??= createGuestToken(); await session.save(); const guest = await indiana.createGuestCustomer({ guestToken: session.indianaGuestToken }); customer = { type: "id", value: guest.customerId } as const; } // Persist before the first request. Retries reuse the whole original input. if (!session.funnelStart) { session.funnelStart = { funnelId: process.env.INDIANA_FUNNEL_ID!, requestId: crypto.randomUUID(), customer, ...(session.userId ? { customerData: { attributes: { plan: session.plan } } } : {}), locale: session.locale, parameters: { commerce: { provider: "stripe", currency: "USD" } }, }; await session.save(); } const result = await funnels.start(session.funnelStart); // The browser runs the Funnel as that Customer. const { customerToken } = session.userId ? await indiana.createCustomerSession(result.customerId!) : await indiana.resumeGuestSession({ guestToken: session.indianaGuestToken! }); session.indianaRunToken = result.runToken; await session.save(); return Response.json({ runToken: result.runToken, customerToken }, { headers: { "Cache-Control": "private, no-store" }, });}展示、重试与身份切换
requireHostSession、session.save、reportError 和购买回调是需要你的应用实现的接入点。商业动作仍调用独立服务;当前配置下不支持的动作须显示明确错误,不能在浏览器标成已完成。
使用已领取的 Coupon 时,向 showFunnelRun 或 showPaywall 传入 resolveOfferId({ actionId, productVersionId }),返回所选 Coupon Grant 的 UUID;常规购买返回 undefined。SDK 在报价前选择一次,并在同一请求重试时保留该优惠。领券不会自动应用折扣,服务端仍校验归属、资格、有效期和 Product。自定义界面可向 session.preview 显式传入 offerId。
- destroy 只清理界面;close 提交关闭命令。路由离开或换号后清理界面、请求与内存凭据,迟到结果不得覆盖新身份。
- Guest 认领保留相同 UUID 时可以 link 继续;claimGuestCustomer 返回 switched=true 时,关闭原 Guest 界面,为返回 Customer 创建会话并启动新流程。历史和持有不会自动合并。
- 每个 Funnel 独立配置 Local/Remote,Owner/Admin 通过单独设置更改。已接收的结果保持固定,新的请求使用当前规则;已下载 Local 规则无法收回。
- Funnel 完成、按钮点击、回跳与自定义回执都不是付款或权益证明。用商业操作状态与 readHoldings 核对结果。
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/indiana-funnel", { method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken, customerToken } = await response.json();const client = createFunnelRunController({ baseUrl: "https://your-indiana-host.example", scope: { projectId: "your-app-uuid", environment: "sandbox" }, runToken,});client.identify(customerToken);const handle = await showFunnelRun(container, { client, // Record-only purchase: your backend creates the Stripe Checkout Session from execution. onPurchase: ({ operation }) => startMerchantCheckout(operation.execution, operation.operationId), // External pages: without onOpenUrl the SDK opens this window or a new one as authored (openIn). onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.client.submit({ operation: "close", requestId: crypto.randomUUID(), nodeId: flow.client.view!.nodeId, visit: flow.client.view!.visit, revision: flow.client.view!.revision });完成后检查
预期结果
保留固定版本、运行凭据和原命令,能区分等待、关闭、失败与完成;恢复不重复执行已接受的商业操作。