路由器就是登錄表
Hanzo Cloud 沒有簽入的 OpenAPI 檔案。活著的路由表才是源頭,朝三個方向投影,任一投影一漂移,雙射測試就讓建置失敗。963 條路徑上的 1,362 個操作,在請求當下按行程產生。

本頁內容
Hanzo Cloud 的儲存庫裡沒有 openapi.yaml。沒有第二份列出端點的登錄表,沒有註解檔,也沒有 CI 裡把規格寫到磁碟的產生步驟。想知道 API 是什麼,就問一個正在跑的行程,它讀自己的路由器來回答。
這篇談的是,為什麼到頭來只有這種安排站得住,以及其中行不通的部分。行不通的那一半,反而比較有用。
一張表,三種投影
serve.go 組成一張路由表。接著三樣東西讀它,都在 MountAll 之後,於是各自看到完整的表:
/zap在 ZAP 傳輸上 重播/v1的處理常式。- 主控台 繪製 它們。
GET /v1/openapi.json描述 它們。
三者都不持有任何副本。這就是全部的設計約束,其餘都從這裡來。自己握著端點清單的投影會變成第二個真相來源,而第二個真相來源會漂移。不是有時,是永遠,照著有人忘記的頻率排程。
描述投影是人們會問的那個,所以:openapi.Live(app) 呼叫 app.Fiber().GetRoutes(true),openapi/ 裡的其他函式都是回傳的 []Route 的純函式。文件不是用路由器蓋出來的。文件 就是 路由器,只是另一種形狀。
為什麼讀活的路由器才是唯一完整的來源
顯而易見的替代是掃描原始碼。它行不通,理由值得講準,因為這正是很多 API 工具悄悄少報的原因。
POST /v1/kms/auth/login 是這樣登記的:
Group("/v1/kms/auth").Post("/login")
沒有 grep 找得到那條路徑。字串 /v1/kms/auth/login 不在程式碼庫裡。它只存在於組裝後的路由器中,在執行時,在框架把群組前綴和葉子接起來之後。
然後事情變得更糟,而這種方式其實是對的。路由集合是部署設定的函式:cfg.Enabled,再加上 KMS 的 if kc != nil 這類內部閘門。不安裝管理功能的部署就不提供管理路由。所以那個部署的誠實規格不會宣傳它們。
所以文件按行程、在請求當下產生,而不是在 CI 裡建置一次。CI 裡建置的規格描述的是一個假想的最大部署,未必是你正在交談的那個。從活路由器讀出的規格描述的是回答你的那個行程。
讓這件事為真的測試
規格不能漂移的架構是一項主張,主張需要一道門。我們的門是雙射,在 cmd/cloud/openapi_test.go:
func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {
它掛載 apps.Wire() 裡的每個子系統,產生文件,並雙向斷言對應:每條活路由都作為操作出現,每個操作都有活路由支撐。任一方向都不是子集。
今天跑一遍:
bijection holds over 1362 operations / 963 paths / 138 products
這是儲存庫裡唯一一個失敗就表示已發布文件在說謊的測試。別的東西壞了,是產品錯了;這個壞了,是契約錯了,更糟,因為整合方已經照著它建過了。
計數移動不是問題。每加一個子系統它就動。要緊的數是兩邊的差,那個數是 0,否則建置失敗。
產品軸是機械的
/v1/ 之後的第一段路徑就是產品。openapi.Product 讀它,並用它給每個操作貼標籤,於是 CLI 可以構造 hanzo <product> <resource> <verb>,而不需要誰去判斷一條命令該住在哪裡。
這是故意不當子系統的名字。clients/billing 也提供 /v1/finance/*,呼叫方看到的是 URL,所以 URL 贏。若按內部套件給這條軸命名,CLI 就會變成一件有意見、得有人維護的策展物,而不是投影。
路由器不能告訴你的事
這就是限度,我們不打算繞過它。
方法、路徑、路徑參數和產品可以從路由器導出。請求與回應的綱要、查詢和標頭參數、狀態碼以及認證則 不能。路由器持有一個 func(*zip.Ctx) error。請求型別是處理常式體內的區域變數:
var req secretPutRequest
json.Unmarshal(ctx.Body(), &req)
Go 無法從函式值反射進它的函式體。這裡沒有巧妙的修法,在產生器裡嘗試就意味著猜測,規格就是這樣得到自信而錯誤的條目。
通往綱要的唯一路徑是帶型別的操作:
zip.Get[In, Out](...)
In 和 Out 在登記上,所以不必讀任何人的心思就能得到綱要。也因為它們在登記上,同一條目還產出一個 MCP 工具。zip/openapi.go 和 zip/mcp.go 是同一份登錄表的兩個讀者。那是第四種投影,它到來時並沒有第四套機制。
遷移按構造是增量的:GetRoutes() 是 app.ops 的超集,所以把處理常式改成帶型別的操作會加上綱要,而不改動它上游的任何東西。
不能加型別的路由,以及原因
clients/integrations 已經轉換到線路允許的地步:41 條裡有 20 條是帶型別的操作。另外 21 條仍是原始處理常式,每一條都列在 ops_projection_test.go 的 rawRoutes 對應裡,並寫明它不能移動的原因:
- 13 個瀏覽器重新導向。 OAuth 回呼和每一段連結都以
302應答。帶型別的操作回傳一個值,而不是Location標頭和 300 段的狀態。 - 6 個 webhook。 Slack 與 GitHub 的 HMAC、Discord 的 Ed25519、Teams Bot Framework 的 JWT、Telegram 的祕密權杖標頭。每一個都在 原始請求位元組 上認證簽章。帶型別的操作收到解碼後的
In,永遠看不到計算簽章所用的位元組,所以給它們加型別會破壞認證。 - 2 個
202 Accepted的建立者。cloud.Created給我們一條 201 的接縫。沒有 202 的接縫。兩條路由需要一條,所以兩條路由等待。
我們認為那份清單是整個系統裡最有價值的成品。它是一份列舉的、被測試釘住的清單,精確標出抽象在哪裡停下,每一條都有審查者可以爭辯的理由。它防止的失敗是:一條路由悄悄滑回原始處理常式,同時從文件、MCP 工具集和 CLI 裡消失,看不見,因為沒有任何東西在斷言它曾經在那裡。
架構不是那些能夠推廣的部分。它是能夠推廣的部分,再加上對不能推廣的部分一份誠實、被強制執行的交代。
這換到什麼
對接 Hanzo Cloud 的呼叫方讀到的文件,是回答他們請求的那個行程、在他們真正交談的部署裡、幾毫秒前產出的。經 MCP 呼叫的代理到達同一個處理常式,帶同樣的租戶檢查,和 REST 呼叫方一樣。一次匿名的 /mcp 呼叫得到 REST 給出的同一個 403,因為那是同一條程式碼路徑,並且有測試把它釘住。
這一切都不需要誰記得去重新產生任何東西。
一般形式是:若兩件成品必須一致,就不要從一件產生另一件然後指望。讓其中一件成為另一件的視圖,並寫下它們不一致時會失敗的測試。
繼續閱讀

Cloud-Native Commerce: Why Architecture Matters
How we built Crowdstart as a cloud-native platform from day one, and why this matters for commerce.

代理型語言模型的時代,無頭式商務要先賣得成
買家正在變成代理人。要讓大型語言模型在沒有人插手時讀得懂、簽得了名、付得了款,需要哪些條件,以及 Hanzo 今天為此準備了什麼。

Introducing Enso — one API, routed to the best frontier intelligence
Enso is Hanzo's learned router: no single frontier model leads every benchmark, so Enso routes each request to the model most likely to win it — and learns from your feedback as it goes. Microsecond routing on a CPU, a transparent per-request meter, and a 1% fee that pays for itself. Here is how it works, the measured results, and the cost-transparency angle a monolith cannot offer.