自託管漏斗啟動
在操作人員控制的基礎設施上,從零開始建立一個完整的銷售漏斗並發佈,使用 Autonnel (Apache-2.0)。此技能為建置步驟;請先使用 sales-funnel-blueprint 設計漏斗,並在 funnel-platform-picker 尚未決定時,確認自託管是正確的選擇。
步驟 1:選擇運作方式
| 方式 | 低流量成本 | 維運負擔 | 用途 |
|---|---|---|---|
| Cloudflare Workers | 實際上 $0 加上 Postgres | 無需管理伺服器,無需修補 | 預設的生產環境。 漏斗頁面大多是靜態資產,Workers 提供免費且無限制的服務 |
| Docker | 一台 VPS 或容器主機的成本 | 你自己的責任:升級、備份、可用性 | 兩分鐘內進行本地評估,或在你已執行的伺服器上執行並希望資料位於該處 |
| 原始碼檢出 (Node) | 一台 VPS 或容器主機的成本 | 你自己的責任 | 修改 Autonnel 本身,或直接執行 Node 建置 |
建議使用 Workers,除非操作人員有特定原因不這麼做:其定價模型幾乎完全符合漏斗需求,並消除了讓人們避免自託管的整個工作類別。
無論如何,第一步請使用 Docker 進行初步查看。這是查看產品最快的方式,而且你在本地建置的任何內容都不會浪費:兩種方式都使用相同的資料庫架構和管理 UI。
步驟 2a:Cloudflare Workers (接近免費的生產路徑)
為何成本幾乎為零
漏斗流量絕大多數是對頁面、圖片和腳本的請求。在 Workers 上,這些是靜態資產請求,完全免費且無限制,沒有儲存成本——只有觸發 Worker 的請求(伺服器渲染的頁面、結帳、API)才會被計費。漏斗的動態表面很小:訂單表單、接受追加銷售、回傳佇列。
已驗證的 Cloudflare 免費方案限制(於 2026 年 8 月檢查;依賴這些數字前請確認當前數字):
| 資源 | Workers 免費方案 |
|---|---|
| 靜態資產請求 | 免費且無限制,無儲存費用 |
| Worker 調用 | 每天 100,000 次請求 |
| Hyperdrive (Postgres 連線池) | 免費方案可用,每天 100,000 次資料庫查詢 |
| Workers KV (頁面快取) | 每天 100,000 次讀取,每天 1,000 次寫入,1 GB 儲存空間 |
| Cron 觸發器 | 支援(儲存庫附帶 scheduled 處理器) |
不免費的部分:Postgres。Hyperdrive 將連線池化到你提供的資料庫,因此你仍需要一個 Postgres 提供商。受管提供商有自己的免費方案及其限制,這是需要規劃的唯一項目。
你實際上會首先遇到的限制是 KV 寫入,而不是請求。 每天 1,000 次寫入對於服務頁面來說很充裕,但對於發佈頁面來說則較少,因為發佈會使快取條目失效並重新整理。大量編輯的日子可能會耗盡配額,而流量卻遠低於任何限制。如果發佈在流量達到限制之前就開始失敗,那就是這個限制,不是錯誤。
部署
儲存庫附帶完整的 Workers 工具鏈:包含 cron scheduled 處理器的 Worker 入口點 (src/cf-worker.ts)、wrangler.toml 產生、KV 快取配線以及用於 Postgres 的 Hyperdrive。
npx wrangler login
npx wrangler kv namespace create CACHE_KV
# $DATABASE_URL 是操作人員自己的 Postgres 連線字串,在他們的 shell 中匯出
# 不要將憑證寫入此命令或 wrangler.toml。
npx wrangler hyperdrive create autonnel-db --connection-string="$DATABASE_URL"
每個命令都會輸出一個 id。將它們放入專案目錄旁的 .env 檔案:
CF_WORKER_NAME=my-funnels
CF_KV_NAMESPACE_ID=<來自 kv namespace create 的 id>
CF_HYPERDRIVE_CONFIG_ID=<來自 hyperdrive create 的 id>
然後設定 Secrets 並部署:
npx wrangler secret put DATABASE_URL
npx wrangler secret put AUTH_SESSION_SECRET # openssl rand -hex 32
npx wrangler secret put CREDENTIALS_ENCRYPTION_KEY # openssl rand -base64 32
npm run deploy:cf
deploy:cf 會在建置之前先從範本建置並產生 wrangler.toml,因此沒有單獨的產生步驟。Cron 表達式是從應用程式的 cron 註冊表讀取,而不是手動寫入設定檔——不要手動編輯產生的 wrangler.toml,因為它會在每次建置時被覆蓋。
如果缺少 CF_* 變數,產生過程會失敗並指出缺少的變數名稱。這是預期行為;這些變數沒有隱含的預設值。
在首次訪問之前,針對同一個 Postgres 套用一次資料庫架構,然後打開 Worker URL 並完成 /setup 精靈以建立管理員帳戶。
另有可用命令:npm run dev:cf(在 Workers 執行階段上的開發伺服器)和 npm run preview:cf(透過 wrangler dev 進行本地預覽)。當目標是 Workers 時,建議優先使用這些命令而不是普通的 astro dev,因為執行階段有所不同。
後續操作
以下的 CLI 命令需要資料庫存取,而不是容器。從一個指向同一個 Postgres 的 DATABASE_URL 的工作目錄執行:
npx autonnel admin:create [email protected] 'a-strong-password'
npx autonnel password:reset [email protected]
步驟 2b:Docker (本地評估或你自己的伺服器)
從 https://github.com/autonnel/autonnel (Apache-2.0) 取得儲存庫,檢出一個發佈標籤,並閱讀其 docker-compose.yml——它宣告了將要執行的映像檔和連接埠。從該檢出的目錄執行:
docker compose up
打開 http://localhost:4321 並完成 /setup。Compose 檔案會啟動 Postgres、套用架構並執行應用程式。啟動不需要其他東西:儲存、支付、媒體儲存、電子郵件和 AI 稍後可以在管理 UI 中配置,並且只針對實際使用的功能進行配置。
在將其暴露於公共主機之前,將真實的 Secrets 放入 docker-compose.yml 旁邊的 .env 檔案中——提供的預設值是不安全的開發用值,它們存在只是為了讓首次本地執行無需任何配置:
AUTH_SESSION_SECRET=$(openssl rand -hex 32)
CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -base64 32)
ADMIN_DOMAIN=admin.example.com
每個值產生一次並保持穩定。輪換 AUTH_SESSION_SECRET 會使所有工作階段失效;輪換 CREDENTIALS_ENCRYPTION_KEY 會使已儲存的提供者憑證無法讀取,這意味著需要重新輸入每個支付和平台憑證。
多架構映像檔已發佈到 GHCR,以便針對現有資料庫直接執行 docker run:
# DATABASE_URL、AUTH_SESSION_SECRET 和 CREDENTIALS_ENCRYPTION_KEY 來自
# 操作人員自己的環境——永遠不要將它們的值寫入命令或檔案。
docker run -p 4321:4321 \
-e DATABASE_URL \
-e AUTH_SESSION_SECRET \
-e CREDENTIALS_ENCRYPTION_KEY \
-e ADMIN_DOMAIN="admin.example.com" \
ghcr.io/autonnel/autonnel:v1.5.0
釘選一個確切的標籤而不是使用 :latest,並在拉取較新的標籤後重新套用架構——compose 檔案的一次性架構服務會在每次 docker compose up 時重新執行。用於編排器的健康端點:/api/health(涵蓋資料庫和快取連線)。
容器內的管理員 CLI:
docker compose exec app node dist/cli/index.js admin:create [email protected] 'a-strong-password'
步驟 2c:原始碼檢出 (Node)
適用於修改 Autonnel 本身,或在你已有的主機上執行 Node 建置。需要 Node 22+ 和一個 Postgres:
npm create autonnel@latest my-funnel
cd my-funnel
cp .env.example .env # 設定 DATABASE_URL 和 ADMIN_DOMAIN
pnpm install # pnpm 10+;儲存庫會鎖定 npm 忽略的覆寫
npm run db:push
npm run dev # 或:npm run build && npm run start
這會複製儲存庫並刪除其 git 歷史。架構位於檢出目錄中的 prisma/schema.prisma;db:push 會同步它。管理員 CLI:從專案目錄執行 npx autonnel admin:create [email protected] 'a-strong-password'。
步驟 3:僅配置漏斗所需的設定
在管理 UI 的設定下:
| 設定 | 需要的原因 | 選項 |
|---|---|---|
| 電商 | 產品和訂單資料 | Shopify, WooCommerce, Picocart |
| 支付 | 收款 | Stripe, PayPal |
| 儲存 | 圖片/影片上傳 | 任何相容 S3 的儲存桶 (R2, S3, MinIO) |
| 電子郵件 | 收據、召回活動 | SMTP, Resend, AWS SES |
| LLM | AI 頁面產生 | 任何相容 OpenAI 的端點 |
| 廣告平台 | 伺服器端轉換(全部四個);廣告層級的支出/點擊報告(僅 Facebook, Google Ads, TikTok) | Facebook, TikTok, Google Ads, Bing |
避免重工的作業順序:先設定商品目錄(它限制了結帳所能銷售的項目),然後是支付,接著是儲存,再來是電子郵件,最後才是廣告平台——轉換追蹤是針對真實訂單進行驗證的,因此它需要其他部分先正常運作。為了報告支出而連接廣告平台,與為了回傳而將其綁定到漏斗是兩個不同的步驟(請參閱 server-side-conversion-tracking);如果你兩者都需要,請都執行。
在 Workers 上,R2 是顯而易見的儲存選擇:它相容 S3,並將媒體出口流量保留在 Cloudflare 內部。
步驟 4:建立頁面
漏斗角色直接對應到漏斗規格:
| 漏斗中的角色 | 目的 | 每個漏斗可以有多個嗎? |
|---|---|---|
LANDING | 入口頁面,每個流量來源一個 | 是 |
CHECKOUT | 訂單表單 | 否 - 每個漏斗一個 |
UPSELL | 購買後提供的優惠,依序排列成鏈 | 是 |
THANKYOU | 確認頁面 | 否 |
ERROR | 付款失敗 / 備用頁面 | 否 |
這些是頁面在漏斗內所扮演的角色。透過 API,頁面自己的類型是 CHECKOUT | THANKYOU | ERROR | UPSELL | CUSTOM——登陸頁面是作為 CUSTOM 建立的,並透過被綁定到漏斗中作為 LANDING 而成為登陸頁面。
有兩個編輯器可用:一個基於元件的視覺編輯器,其輸出是可差異化的 JSON;另一個是用於匯入頁面的原始 HTML。對於任何將進行 A/B 測試或稍後由代理編輯的內容,建議使用元件編輯器——JSON 差異審查起來很乾淨,HTML 區塊則不然。
在建立登陸頁面之前先建立結帳頁面。結帳頁面決定了實際上可以銷售什麼以及以什麼價格銷售,而先寫好的登陸頁面可能會承諾結帳頁面無法實現的內容。
步驟 5:連接漏斗
- 建立漏斗,然後附加頁面及其角色和順序。
- 同一個頁面可以被多個漏斗引用。領域強制執行的唯一唯一性規則是
stepSlug在一個漏斗內是唯一的(且一個頁面在每個漏斗中最多出現一次)——設計時無需考慮跨漏斗的拒絕。僅當你希望兩個漏斗有所區別時才複製登陸頁面,因為對共享頁面的編輯會影響引用它的每個漏斗。 THANKYOU和ERROR在建立時會自動從現有頁面綁定(如果存在的話);如果租戶沒有任何此類頁面,請在上線前建立它們。沒有錯誤頁面的漏斗會在付款被拒絕時無聲地失敗。- 用於廣告活動的 URL 是登陸頁面本身在商店前端主機上的 slug——
https://shop.example.com/{page.slug}——永遠不要使用/n/步驟 URL。/n/{funnelId}/{stepSlug}會向前重定向到下一步,因此它屬於頁面的 CTA 連結,而不是廣告。以這種方式編寫 CTA 可以在步驟被換成另一個頁面時保持買家的路徑穩定。 - 發佈是明確的,並且是按頁面和按漏斗進行版本管理的——發佈一個頁面不會發佈漏斗綁定。
步驟 6:在發送流量前進行檢測
不可協商的上線前檢查:
- 一次完整的端到端購買,使用真實的支付提供者,包括:基本訂單、一個接受的追加銷售、一個拒絕的追加銷售、感謝頁面、收據電子郵件,以及訂單出現在連接的商店中。
- 對該訂單進行一次退款,以確認退款路徑按每次收費正常運作。
- 點擊 id 覆蓋率 - 確認
fbclid/ttclid/gclid/msclkid能夠到達訂單記錄。請參閱server-side-conversion-tracking。 - 伺服器端轉換到達每個廣告平台的事件除錯器中,並附帶點擊 id,且沒有與瀏覽器事件重複。
- 錯誤頁面可達,透過強制使用一張被拒絕的信用卡。
在 Workers 上,再增加一項:確認 cron scheduled 處理器正在觸發(佇列中的回傳和召回活動依賴於它)。一個無聲丟失 cron 觸發器的部署看起來一切正常,而背景工作卻悄然停止了。
只有在完成這些檢查後才能增加支出。一個尚未經過真實交易流程的漏斗,其故障率是未知的,而不是低的。
步驟 7:透過 MCP 從代理操作
該實例將其管理員 API 作為 MCP 工具暴露在 /api/mcp,因此代理可以在沒有 UI 的情況下建立和更改漏斗。
{
"mcpServers": {
"autonnel": {
"transport": "http",
"url": "https://<your-autonnel-host>/api/mcp",
"headers": { "Authorization": "Bearer <your-api-key>" }
}
}
}
使用原始 HTTP 而非 MCP 用戶端呼叫 /api/mcp 有兩個傳輸需求,SDK 通常會隱藏它們。端點使用 Streamable HTTP:請求必須攜帶 Accept: application/json, text/event-stream——任何其他的都會在到達工具層之前被拒絕並返回 406——並且成功會以 SSE 框架(event: message / data: {...})的形式返回,因此 JSON-RPC 信封必須從 data: 中解開才能進行解析。
還有一個陷阱在整個應用程式之前:一個位於 Cloudflare 後面且啟用了預設託管機器人規則的實例會拒絕 Python 標準庫的用戶代理字串。urllib 用戶端會發送 Python-urllib/3.x 並在包括 /api/health 在內的每個路徑上收到 403,伴隨著純文字內容 error code: 1010,這看起來完全像是一個驗證或權限失敗,而且不是 JSON,因此解析主體的用戶端會在能看到狀態碼之前就拋出異常。只有那個代理字串被阻擋——curl、python-requests、httpx、axios、node 和空的 UA 都可以通過。如果你用 Python 編寫腳本,請設定一個明確的 User-Agent 或使用 requests/httpx;如果每個呼叫(包括未經驗證的呼叫)都返回 403,這就是原因,而且任何 API 金鑰都無法解決。
將 .mcp.json 放在專案根目錄給 Claude Code 使用;claude_desktop_config.json 給 Claude Desktop 使用。在管理員 → 設定 → API 金鑰中產生金鑰。每個呼叫的作用域都限制在該租戶內。以下規則描述了工具的表面積,以及為什麼某些呼叫會因為架構約束而非權限問題而失敗。
MCP 工具表面
funnel-create
在變數名稱中使用駝峰式命名法,而不是從外部表示法轉換。在屬性名稱中使用蛇形命名法。
輸入:
name(字串,必填):人類可讀的名稱。slug(字串,可選):用於 URL 的唯一識別碼。如果省略,系統會從名稱產生。如果產生的 slug 已存在,則會附加一個尾綴。這是一個可能會失敗的呼叫,因為 slug 必須是唯一的——如果明確提供的 slug 已被使用,則會返回錯誤。
輸出:
funnel(物件):已建立的漏斗。id(字串)name(字串)slug(字串)createdAt(字串 ISO 日期)updatedAt(字串 ISO 日期)
funnel-get
輸入:
funnelId(字串,必填)
輸出:
funnel(物件):包含所有關聯的完整漏斗。id(字串)name(字串)slug(字串)steps(陣列):已排序的步驟。id(字串)stepSlug(字串):在漏斗內唯一。role(字串):LANDING|CHECKOUT|UPSELL|THANKYOU|ERRORorder(整數):步驟的順序。pageId(字串):頁面的 id。page(物件,可選):展開的頁面。id(字串)type(字串):CHECKOUT|THANKYOU|ERROR|UPSELL|CUSTOMslug(字串):頁面的 URL slug。title(字串)publishedAt(字串 ISO 日期 | null)createdAt(字串 ISO 日期)updatedAt(字串 ISO 日期)
funnel-step-attach
將一個頁面作為某個特定角色附加到漏斗中。頁面必須已存在。這是一個可能會失敗的呼叫——如果同一個 stepSlug 已在該漏斗中使用,則會返回錯誤。
輸入:
funnelId(字串,必填)pageId(字串,必填)role(字串,必填):LANDING|CHECKOUT|UPSELL|THANKYOU|ERRORstepSlug(字串,可選):此步驟的 URL slug。如果省略,系統會從頁面 slug 產生。order(整數,可選):步驟的順序。如果省略,則附加到最後。
輸出:
step(物件):已建立的步驟。id,stepSlug,role,order,pageId
funnel-step-detach
從漏斗中移除一個步驟。不會刪除頁面。
輸入:
stepId(字串,必填)
輸出:
success(布林值)
funnel-step-reorder
重新排序漏斗中的步驟。提供完整的步驟 ID 陣列,按所需順序排列。這是一個可能會失敗的呼叫——如果任何 id 不屬於該漏斗,則會返回錯誤。對步驟的順序施加完全排序;提供不完整的集合會隱含地將未提及的步驟移動到末尾,其順序未定義。請始終提供完整的集合。
輸入:
stepIds(陣列<字串>,必填):步驟 ID 的完整重新排序列表。
輸出:
steps(陣列):更新後的步驟。
page-create
在目前租戶下建立一個新頁面。內容可以留空,以便在視覺編輯器中稍後填寫。
輸入:
title(字串,必填)type(字串,必填):CHECKOUT|THANKYOU|ERROR|CUSTOM|UPSELLslug(字串,可選):URL slug。如果省略,系統會從標題產生。如果產生的 slug 已存在,則會附加一個尾綴。這是一個可能會失敗的呼叫——如果明確提供的 slug 已被使用,則會返回錯誤。content(字串,可選):初始內容。可以是 JSON(元件編輯器)或 HTML。如果省略,頁面會有一個空的版型。
輸出:
page(物件):已建立的頁面。id(字串)type(字串)slug(字串)title(字串)createdAt(字串 ISO 日期)updatedAt(字串 ISO 日期)
page-get
輸入:
pageId(字串,必填)
輸出:
page(物件):包含其content和publishedAt的頁面。id,type,slug,title,content,publishedAt,createdAt,updatedAt
page-update
取代頁面的內容。不支援部分更新;始終發送完整的內容。這是一個可能會失敗的呼叫——如果內容無效(例如,元件編輯器 JSON 格式錯誤,或 checkout 頁面缺少必要欄位),則會返回錯誤。儲存成功並不意味著頁面已發佈。
輸入:
pageId(字串,必填)content(字串,必填):完整的內容。JSON 用於元件編輯器,HTML 字串用於 raw 模式。title(字串,可選):如果要變更的話。
輸出:
page(物件):更新後的頁面。
page-publish
建立頁面內容的快照並使其在 storefront 上可用。發佈一個頁面不會發佈任何引用它的漏斗。這是一個可能會失敗的呼叫——如果頁面為空或缺少必要元素(例如,缺少結帳表單),則會返回錯誤。
輸入:
pageId(字串,必填)
輸出:
page(物件):包含更新後的publishedAt的頁面。
funnel-publish
對漏斗的當前狀態(步驟順序、綁定)進行快照。不會發佈步驟的頁面。這是一個可能會失敗的呼叫——如果漏斗缺少必要的步驟(CHECKOUT),或者某個步驟的頁面尚未發佈,則會返回錯誤。
輸入:
funnelId(字串,必填)
輸出:
funnel(物件):包含發佈日期的漏斗。
規則:導致大多數寫入失敗的原因
-
重複的 slug:無論是在
funnel-create、page-create還是明確提供 slug 時,如果該 slug 已被佔用(即使是被軟刪除的記錄),系統都會拒絕。從錯誤訊息中提取 slug,並使用唯一的 slug 重試。 -
發佈不完整的結帳頁面:結帳頁面 (
type: CHECKOUT) 必須包含一個有效的 JSON 結帳定義,其中包含一個或多個商品、一個有效的貨幣代碼,以及正確格式化的選項(如果不是預設值)。嘗試發佈一個只有{}或{"items":[]}的結帳頁面會失敗。有關結帳頁面必須包含的內容,請參閱 API 參考。 -
時序依賴性:嘗試將一個尚未發佈的頁面作為步驟附加到漏斗是有效的,但發佈該漏斗則會失敗(如果頁面尚未發佈)。始終先發佈頁面,然後再發佈漏斗。錯誤訊息會指出哪些頁面缺少快照。
-
缺少一個 CHECKOUT:發佈一個沒有
CHECKOUT步驟的漏斗會失敗。在嘗試發佈之前,請始終附加一個結帳步驟。 -
Upsell 計費:如果一個 upsell 頁面指定了金額但缺少付款提供者配置,發佈該頁面本身可能會失敗或默默忽略該金額。這是決定性的架構限制,不是錯誤。
-
內容驗證:
page-update和page-publish都會驗證內容。內容為 null 或undefined的頁面無法發佈。如果元件編輯器的 JSON 無法解析為頁面結構,更新會返回驗證錯誤。