onagent/docs
串接指南

把 onagent 串進你的應用

onagent 讓 LLM 直接操作你真實的網頁介面。你把應用能做的事描述成一組 tool——具名的動作,帶有型別化的參數——把這份定義推送到平台, 再嵌入一個小小的瀏覽器端 SDK,由它把進來的 tool call 派發給你寫的 handler。 這一頁會走完整條路:取得 CLI、建立 app、定義 tool、接上前端。

總覽

各個元件如何組合起來

一次串接由四樣東西構成,正好對應這一頁的四個章節。

  1. onagent CLI — 一個命令列工具,讓你在終端機(或指令稿/CI 工作)裡完成登入與管理 app,而不必只能透過瀏覽器的 console 操作。
  2. console 裡的一個 app — 一個命名空間(appId),底下擁有一把 API key、一個 allowed origin、一組 tool,以及選填的自訂 system prompt。
  3. tool 定義 — 一份 YAML 檔(或用 console 的 tool 編輯器),描述你的頁面對外提供的每項能力:名稱、一段供 LLM 判斷何時該呼叫它的說明,以及一個 JSON Schema 形式的參數結構。
  4. 瀏覽器端 SDK(@onagent/bridge) — 嵌在你的頁面裡,它會對後端開一條 WebSocket、註冊你的 tool handler,並讓你呼叫 bridge.prompt(text),由 LLM 推理該請求,再把 tool call 派發回你的頁面。

執行時的流程是:你的頁面呼叫 bridge.prompt("..."),後端的推論服務決定要呼叫你宣告的哪些 tool (也可能都不呼叫),SDK 透過 WebSocket 收到該呼叫,執行你註冊的對應 handler,再把結果回報給後端。

步驟 1 — CLI

取得 onagent CLI 並登入

onagent 是 console API 的命令列客戶端。它能做的每件事,也都可以透過瀏覽器的 console 完成—— 看哪個比較合你的工作流程就用哪個。一旦開始寫指令稿(CI、批次推送 tool 等),CLI 會快得多。

安裝

如果你的環境有 Go 工具鏈,直接從 module 路徑安裝:

terminal
$ go install github.com/tim72117/onagent/cmd/onagent@latest

這會在你的 Go bin 目錄裡放一個 onagent 執行檔。不需要另外下載、也沒有獨立的套件登錄中心—— CLI 就住在它所連線的後端的同一個儲存庫裡。

你在用 Claude Code 嗎?

請看下方關於 Claude Code skill 的說明——它可以幫你把整段設定流程跑完。 它內建的預編譯 onagent 執行檔只涵蓋 Windows;在 macOS/Linux 上會退回用 go install 或本機編譯,跟上面的做法一樣。

登入

onagent 支援兩種登入方式。兩者最後都會把 bearer token 快取在本機(放在你的個人設定目錄裡), 所以之後的每個指令都不會再問你一次。

onagent login --web [-api <url>] [-console <url>]

開一個瀏覽器分頁讓你核准登入,接著由本機一次性的 callback 伺服器接收 token——token 本身不會出現在網址列, 也不會經過瀏覽器。這是互動式終端機環境建議的預設做法,也是唯一能與瀏覽器 console UI 共用登入狀態的方式。

onagent login [-api <url>]

直接在終端機裡輸入 email 與密碼,完全不經過瀏覽器。適合沒有瀏覽器可用的無頭環境 (只有 SSH 的機器、CI runner)。

terminal
# 互動式環境建議用這個
$ onagent login --web

# 無頭環境/沒有瀏覽器可用
$ onagent login

用這個指令確認是否成功:

terminal
$ onagent app list
# 只要列出 app 清單(就算是空的)就代表已經登入。
# 出現 "not logged in" 代表 onagent login 還沒成功。

預設連線的伺服器

每個 onagent 指令預設都會連到 https://onagents.dev (-api 與 login --web 用到的 -console 皆是)。 兩者都可以用 -api/-console 旗標覆寫。這裡刻意沒有提供環境變數的覆寫方式—— 旗標是唯一能改變連線目標的途徑。

完整指令參考

每個子指令都接受一個選填的 -api <url> 旗標;下表為求簡潔而省略。

指令參數用途
login—在終端機裡輸入 email/密碼登入。
login --web[-console <url>]透過瀏覽器分頁登入;token 由本機的 callback 伺服器交換取得。
app list—列出你的 app,並顯示各自的 tool 數量與是否已有 key。
app create<appId>建立一個新的 app 命名空間。
app delete<appId>刪除一個 app 及其底下的所有東西(tool、key、origin)。無法復原。
app origin set<appId> <origin>設定這個 app 的 WebSocket 連線所允許的瀏覽器 origin(必須完全相符)。
app thought set<appId> <thought>設定這個 app 的自訂 system prompt(傳空字串則是清除)。
app maxpromptlength set<appId> <value|clear>把這個 app 的終端使用者 prompt 上限設為 value 個字元,或用 clear 退回系統層級的預設值。只能收緊系統層級的上限,永遠不能放寬。
key issue<appId>為這個 app 產生一把新的 API key,只顯示一次。會立刻撤銷先前的 key。
key revoke<appId>立刻撤銷這個 app 目前的 API key。
tool list<appId>以 YAML 印出這個 app 目前的 tool 定義。
tool create<appId> <tool.yaml>驗證並上傳一份本機 YAML 檔裡的 tool 定義,新增它、或取代同名的既有 tool——這個 app 底下的其他 tool 不受影響。
tool delete<appId> <toolName>依名稱從 app 移除一個 tool。

appId 必須符合 ^[a-zA-Z0-9][a-zA-Z0-9_-]*$——以英文字母或數字開頭, 之後可以是英文字母、數字、-、_ 的任意組合。

步驟 2 — Console

建立 app、發放 key、設定 allowed origin

這三件事都可以用 CLI 或 console 的網頁 UI 完成——哪個方便就用哪個;兩者操作的是同一筆 app 紀錄。

1

建立 app

terminal
$ onagent app create my-app
Created app "my-app".

或是在 console 裡:登入 /app 後點 + New app。

2

發放 API key

terminal
$ onagent key issue my-app
API key for "my-app" (shown once — copy it now, it can't be retrieved again):
  sk_live_ab12cd34...
Issuing a new key later immediately revokes this one.

金鑰只會顯示這一次

後端只會儲存金鑰的雜湊值,從不保存明文——所以它一旦從終端機捲走(或你關掉 console 的金鑰視窗), 就永遠找不回來了。重新發放是取得新金鑰的唯一方式,而這麼做會 立刻讓前一把金鑰失效。不要隨意輪替正式環境 app 的金鑰—— 你一按下去,所有還在用舊金鑰的連線都會當場中斷。

3

設定 allowed origin

terminal
$ onagent app origin set my-app https://your-site.example.com
Set "my-app"'s allowed origin to "https://your-site.example.com".

請填你的頁面實際被服務的那個 origin——協定 + 主機 + 連接埠,不要帶路徑、 也不要有結尾斜線(例如 https://your-site.example.com,而不是 https://your-site.example.com/ 或 .../app)。在 console UI 裡, 同一個欄位在該 app 的頁面上標示為 Allowed origin。

origin 採 fail-closed——這是最常見的求助問題

一個沒有設定 allowed origin 的 app,會直接拒絕每一條 WebSocket 連線,就算 API key 完全正確也一樣。只要有金鑰參與,就沒有「全部允許」的退路,也沒有開發模式的 豁免——origin 未設定,等於這個 app 不接受來自任何地方的連線。如果你(或使用者)回報 「API key 明明是對的,但 WebSocket 就是連不上」,或是連線莫名失敗又沒有其他錯誤訊息, 第一件要檢查的,就是這個 app 的 allowed origin 有沒有設定、 以及是否與頁面實際的 origin 完全一致(含協定與連接埠)。

步驟 3 — Tools

定義 tool 並推送上去

一個 tool 就是你的頁面對 LLM 開放的一項能力:一個供它呼叫的名稱、一段告訴模型何時該用它的說明, 以及一組 JSON Schema 形式的參數。定義 tool 有兩種方式——為每個 tool 手寫一份 YAML 檔, 再用 onagent tool create 推上去;或是使用 console 內建的 tool 編輯器。 兩者寫入的是同一份底層定義;看你有多少個 tool、以及想不想把它們納入版本控制來決定。 onagent tool create 一次依名稱新增或取代一個 tool——絕不會動到這個 app 的其他 tool。

tool.yaml 結構

一份檔案描述一個 tool——以下是它的欄位:

欄位必填說明
name是LLM 呼叫這個 tool 時使用的識別名稱。必須符合 ^[a-zA-Z_][a-zA-Z0-9_]*$,且在同一個 app 裡不可重複。
description是告訴 LLM 何時、為何該呼叫這個 tool。這是模型在多個 tool 之間做選擇時最主要的依據——請寫得具體。
parameters是一個 JSON Schema 物件:type: object、一個 properties 對應表(每一項都要有 type——string、number、integer、boolean、array 或 object——以及選填的 description),還有選填的 required 屬性名稱清單。型別為陣列的屬性用 items 描述;型別為物件的屬性可以再巢狀一層 properties/required。任何屬性也都可以加上選填的 enum——一份允許的字串值清單——把 LLM 限制在固定選項之中,而不是自由填寫。
kind否action(預設)或 query——見下方 action 與 query。
search_products.yaml
name: search_products
description: Search the product catalog by keyword.
parameters:
  type: object
  properties:
    query:
      type: string
      description: The search keywords.
    maxResults:
      type: integer
  required:
    - query
kind: query   # LLM 需要拿回實際的搜尋結果

第二個 tool,沒有寫 kind(預設為 action):

add_to_cart.yaml
name: add_to_cart
description: Add a product to the current user's shopping cart.
parameters:
  type: object
  properties:
    productId:
      type: string
      description: The product's unique ID.
    quantity:
      type: integer
      description: How many units to add. Defaults to 1 if omitted.
    giftWrap:
      type: string
      enum: [none, standard, premium]   # 把 LLM 限制在這三個選項之中
  required:
    - productId
# 省略 kind → 預設為 "action"

action 與 query — 兩者都會阻塞,差別在 LLM 看得到什麼

kind 欄位決定兩種呼叫流程。以目前的後端而言,兩者都是阻塞式的—— tool call 一定會讓進行中的請求等到你的頁面回應為止。差別在於那個回應會被怎麼處理,而不在於平台會不會等它。

kind會阻塞?LLM 拿到什麼
action (預設)會只有「這次呼叫成功或失敗」,不包含你的 handler 回傳的任何資料。適合會產生副作用的操作:送出表單、跳頁、按下按鈕、加入購物車。
query會你的 handler 實際回傳的值,會被餵回 LLM 的推理過程,讓它能依據頁面的真實狀態行動——那是它沒有其他途徑能得知的(例如「目前選了什麼」「搜尋回傳了什麼」)。請節制使用,原因見下方。

query 類型的 tool 會佔住後端共用的鎖

一次 query 類型的呼叫,會在你的頁面回應之前一直佔住後端那把唯一的共用 orchestrator 鎖—— 一個緩慢或沒有回應的分頁,會拖住該後端上所有 app 的所有使用者,而不只是你自己的請求。 請把 kind: query 保留給「LLM 確實需要只有你的頁面才有的資料」這種情況; 凡是「做這件事,然後告訴我成功與否」的操作,一律用 action(或乾脆省略 kind)。

推送 tool

把上面每份 YAML 各自存成一個檔案,再一個一個推上去——這會新增該 tool,或在同名 tool 已存在時取代它:

terminal
$ onagent tool create my-app search_products.yaml
Saved "search_products" to "my-app".
$ onagent tool create my-app add_to_cart.yaml
Saved "add_to_cart" to "my-app".

onagent 會先在本機驗證檔案,通過才送出。最常見的驗證失敗原因:

  • tool 名稱不合法 — 不符合 ^[a-zA-Z_][a-zA-Z0-9_]*$(不能有連字號、不能以數字開頭、不能有空白)。
  • tool 缺少 description。
  • 缺少 parameters.type — 整個 parameters 區塊(或它的 type 欄位)被省略了。

要讀回目前已儲存的內容(例如拿來跟本機檔案比對差異,或從 console 編輯器的狀態產生一份新檔案):

terminal
$ onagent tool list my-app

要從 app 移除一個 tool:

terminal
$ onagent tool delete my-app add_to_cart

設定自訂的 thought(system prompt)

除了各個 tool 自己的說明之外,一個 app 還可以帶有它自己給「負責挑選 tool 的 agent」的自訂指示—— 語氣、領域規則,任何超出「呼叫對應的 tool」之外的要求:

terminal
$ onagent app thought set my-app "Always confirm destructive actions before calling them."
Set "my-app"'s thought.

傳入空字串即可清除它,退回平台的預設值。

限制 prompt 的長度上限

平台會對「單一則終端使用者 prompt 可以有多少字元」施加一個系統層級的上限(由營運後端的人透過 MAX_PROMPT_LENGTH 環境變數設定)。每個 app 還可以另外設定自己更嚴格的上限—— app 自己的設定只能縮小實際生效的上限,永遠無法把它放寬到超過系統層級的值:

terminal
$ onagent app maxpromptlength set my-app 200
Set "my-app"'s max prompt length to 200 characters.

傳入 clear 而不是數字,即可移除這個 app 專屬的上限,退回系統層級的預設值。 超過實際上限的 prompt 會被拒絕,但連線會保持開啟——SDK 會收到一個帶有 prompt_too_long 代碼的錯誤,形式與配額不足的拒絕相同。

步驟 4 — Playground

寫任何前端程式碼之前先試跑

只要一個 app 已經定義好 tool,console 的 Playground 就能讓你對它送出 prompt,並觀察 LLM 如何決定 要不要呼叫、以及怎麼呼叫這些 tool,完全不需要先架好一個真的頁面。在 console 裡選定該 app, 再從側邊欄開啟 Playground(在手機版是固定在底部的按鈕)。 它走的是與 @onagent/bridge 相同的後端 session/tool call 協定, 所以在這裡成功的 tool call,等到真的有頁面回應時行為也會一致——唯一的差別只是「另一端由誰產生 tool_result」。

Mock 範本

這個 tool 有…Playground 的行為
一個 click_button 或 fill_form 範本會繪出一個真的、可點的模擬介面(按鈕或文字欄位,依該 tool 的 enum 選項建立)——LLM 呼叫這個 tool、以及你自己動手點,走的是同一條程式碼路徑,產生的結果也相同。
沒有 mock等待 2 秒,然後回報這次呼叫失敗,並附上明確的「這裡沒有任何東西能真的執行它」訊息——提醒你這個 tool 需要一個真實的頁面來回應。query 類型的 tool 也一樣:Playground 不會捏造答案,因為編造的答案比誠實的失敗更糟。

這兩個內建的 mock 範本來自 console 的 tool 建立精靈,而不是手寫的 YAML——把一個 tool 建成 click_button 或 fill_form 範本,會鎖定它的參數名稱 (label,或 field/value),但 enum 選項仍可自由編輯, 而 Playground 的模擬按鈕/欄位會立即跟著更新。

步驟 5 — 前端 SDK

嵌入 SDK 並實作 tool handler

@onagent/bridge 是瀏覽器端的客戶端:它會對你的 onagent 後端開一條 WebSocket、 註冊你提供的 tool handler,並提供 prompt(text) 來發動一次請求。 在連線就緒之前發出的呼叫,它會先緩衝起來,等連線開啟後再一次送出, 所以你不需要自己去檢查什麼「ready」旗標。

安裝

terminal
$ npm install @onagent/bridge

建立 bridge

app.ts
import { AgentBridge } from "@onagent/bridge";

const bridge = new AgentBridge({
  url: "wss://onagents.dev/ws",
  appId: "my-app",
  apiKey: "YOUR_APP_API_KEY",
  tools: {
    search_products: async ({ query, maxResults }) => {
      const results = await searchCatalog(query, maxResults);
      return results; // kind: query → 這個值會被餵回給 LLM
    },
    add_to_cart: ({ productId, quantity }) => {
      cartStore.add(productId, quantity ?? 1);
      // kind: action(預設)→ 只有成功/失敗會傳到 LLM,
      // 這裡回傳的任何值都會被模型忽略。
    },
  },
  onAssistantMessage: (text) => showInChat(text),
  onError: (err) => console.error("onagent error", err),
});

建構子選項:

選項必填說明
url是WebSocket 端點,例如 wss://onagents.dev/ws。請一律使用 wss://,絕不要用 ws://——API key 是掛在握手網址的查詢參數上(瀏覽器無法為 WebSocket 升級請求附加自訂標頭),所以未加密的連線會讓它以明文出現在網路上,而且往往也會出現在伺服器的存取紀錄裡。
appId是這個 session 要載入哪一個 app 的 tool 組合。
apiKey否onagent key issue 產生的金鑰。有設定時,授權一律以它為準、覆蓋 appId(後端會從金鑰在伺服器端解析出真正的 appId)。只有在連線到明確設定為允許開發/免驗證模式的後端時,才可以省略。
tools是可以是你自己組出來的 Record<string, ToolHandler> 對應表,也可以是一個 ToolEntry[] 陣列(見下方 defineTool)——建構子會把兩種形式正規化成相同結果,並一併檢查名稱是否重複。詳見實作 tool handler。
onAssistantMessage否(text: string) => void — 收到要顯示給使用者看的自然語言訊息時呼叫(例如繪製到聊天面板裡)。
onError否(err) => void — 發生與特定呼叫無關的協定/推論錯誤時呼叫。
onQuotaExceeded否(err) => void — 當一則 prompt 因為 app 擁有者已用盡當月配額而被拒絕時,會改呼叫這個而非 onError。連線會保持開啟——方案升級後,同一條連線上的後續 prompt 就能再次運作。可以用它來顯示升級提示。若未設定,配額錯誤會如同其他錯誤一樣落到 onError。
disconnectWhenHidden否頁面隱藏時(切到其他分頁、視窗最小化)關閉連線,回到前景時重新開啟。預設為 true。WebSocket 存在多久,就等於一個請求開著多久,後端會依此計費整段時間——而隱藏中的頁面本來就不可能把 tool 的結果顯示給任何人看。隱藏期間送出的訊息會進入佇列,重新連上時一併送出,呼叫端不會察覺差異。
lazyConnect否等到第一次送出訊息才建立連線,而不是在建構時就連。預設為 false。如果 bridge 放在多數訪客根本不會互動的頁面上,這個值得開啟——否則每一次瀏覽都會開一條只是拿來計費的連線。所有送出的訊息本來就會排入佇列等連線就緒,所以唯一的代價是第一則訊息要多等連線建立的時間。
minBackoffMs / maxBackoffMs否重新連線的退避時間上下限(毫秒)。預設為 500ms 到 10s。
beaconUrl否當頁面被隱藏/卸載時,用來以 sendBeacon 盡力送出仍在佇列中的訊息的 HTTP 端點——因為 WebSocket 會在傳送完成前就關閉。不需要這個備援機制就省略它。

實作 tool handler

tools 裡的每一個鍵,都必須對應到你已推送到平台上的某個 tool 名稱 (透過 tool create 或 console 編輯器)。handler 會收到該次呼叫已解析的參數, 並依該 tool 的 kind 回傳(或 resolve 出)合適的值:

  • 對 action 類型的 tool,回傳值會被 LLM 忽略——只有你的 handler 有沒有拋出例外會決定成功或失敗,別指望模型看得到回傳的內容。
  • 對 query 類型的 tool,你回傳的任何東西(同步回傳、或由 Promise resolve)都會被序列化並直接餵回 LLM 的推理過程。沒有任何地方會事先宣告它的形狀,所以請回傳「這則 prompt 真正需要的東西」。
  • 拋出例外(或非同步 handler 的 Promise 被 reject)會把失敗連同錯誤訊息一起回報給後端。

只有你註冊過的 handler 才可能被執行

SDK 拒絕呼叫任何不在你傳入的 tools 對應表裡的東西——它沒有 eval、也沒有動態派發的路徑。 如果後端宣告了某個 tool、而你的頁面沒有為它註冊 handler,SDK 會在 console 印出警告 (backend declares tools with no registered handler: ...),而不是等到真的被呼叫時才無聲失敗, 好讓這種不一致及早浮現。

用 defineTool 寫具型別的 handler

把 tools 寫成一個普通物件,代表每個 handler 都是從 args: any 開始—— 型別系統會相信你斷言的任何形狀,而沒有人在執行時真正驗證過它。 defineTool 把 handler 與一個 parseArgs 函式配成一對, 由後者同時驗證原始酬載、並賦予 Args 型別,於是 handler 本身就是完整具型別的:

app.ts
import { AgentBridge, defineTool } from "@onagent/bridge";

interface SearchArgs { query: string; maxResults?: number; }

function parseSearchArgs(raw: unknown): SearchArgs {
  const r = raw as Partial<SearchArgs>;
  if (typeof r?.query !== "string") throw new Error("query must be a string");
  return { query: r.query, maxResults: r.maxResults };
}

const searchProducts = defineTool(
  "search_products",
  parseSearchArgs,
  async ({ query, maxResults }) => { // 完整具型別:是 SearchArgs,不是 any
    return await searchCatalog(query, maxResults);
  }
);

const bridge = new AgentBridge({
  url: "wss://onagents.dev/ws",
  appId: "my-app",
  apiKey: "YOUR_APP_API_KEY",
  tools: [searchProducts], // ToolEntry[],不是 { name: handler } 對應表
});

Args 的真正來源是 parseArgs 的回傳型別,而不是一句光禿禿的 as Args 斷言。它可以像上面那樣是幾行手寫的驗證,也可以包一層 schema 函式庫的 .parse 方法;SDK 不會替你決定。parseArgs 拋出的例外會原封不動地往外傳出該 handler, 因此會被當成一般的 { ok: false, error } tool 結果回報,與其他 handler 錯誤的處理方式完全相同—— 不需要再學一套獨立的錯誤處理路徑。

如上所示,tools 可以直接接受一個 ToolEntry[] 陣列——建構子會把它轉換成 與普通物件相同的 Record<string, ToolHandler> 形式,內部使用的是對外匯出的 toToolRecord 輔助函式。如果你需要先把多個來源的 tool 陣列合併起來,再交給 AgentBridge,也可以自己呼叫 toToolRecord。兩筆共用同一個 name 的項目 ——不論是在同一個陣列裡、或跨越多個被合併的陣列——都會立刻拋出例外, 而不是像 { ...a, ...b } 那樣讓後面的鍵無聲地蓋掉前面的。

送出 prompt

建立完成後,用自然語言的請求呼叫 bridge.prompt(text)。整個呼叫就是這樣—— 它只接受文字,沒有別的參數:

app.ts
searchInput.addEventListener("submit", (e) => {
  e.preventDefault();
  bridge.prompt(userInput.value);
});

LLM 會推理這則 prompt、從你宣告的 tool 中挑出零個或多個來呼叫,SDK 則自動把每次呼叫派發給對應的 handler。 結果與任何自然語言回覆,會透過 onAssistantMessage/onError 以及你自己 handler 的副作用浮現出來——沒有另一個「context」參數可以跟著 prompt 文字一起傳; 模型推理所需要的任何狀態,都應該改由一次 query 類型的 tool call 回傳給它。

呼叫 bridge.close() 可以永久關閉這條連線——之後不會再嘗試重新連線。

另一條路

在用 Claude Code 嗎?有一個免設定的 skill

如果你是在 Claude Code 裡面串接 onagent,onagent-cli-setup 這個 skill 可以帶你走完整段流程—— 偵測安裝狀態、登入、建立 app、發放金鑰、設定 origin,以及推送 tools.yaml。 它是上述所有步驟的互補路徑,而不是替代品:不論走哪一條,底層的 CLI 指令與 tools.yaml 結構都完全相同,所以就算由 Claude Code 代勞實際操作,這一頁仍然值得讀。

安裝這個 skill

它以 npm 套件形式發布,內含五個平台的預編譯 onagent 執行檔 (Windows、macOS Intel/Apple Silicon、Linux x64/arm64)——在任何一個平台上都不需要另外 go install 或自行編譯。

terminal
# 專案層級:.claude/skills/onagent-cli-setup/
$ npx -p @onagent/claude-skill claude-skill-onagent

# 改成使用者層級:~/.claude/skills/onagent-cli-setup/
$ npx -p @onagent/claude-skill claude-skill-onagent --user

這是一次明確的、由使用者主動觸發的安裝(而不是 postinstall 掛鉤)—— 執行它是它唯一會碰到你 .claude/ 目錄的時機。日後重新執行會覆蓋已安裝的版本, 這也就是更新這個 skill 的方式。

參考

疑難排解

針對最常遇到的問題的快速解答。

症狀可能原因
WebSocket 連不上,但 API key 看起來是對的有幾種情況會各自導致握手被拒——無效或過期的 token、不存在的 appId、配額用盡,或是這個 app 被停用了(console 設定頁的狀態)——但 allowed origin 未設定或不相符是其中很常見的一種,值得優先檢查。見上方 origin 採 fail-closed。
onagent 顯示 "not logged in"重新執行 onagent login --web(或 onagent login);快取的 token 可能不存在或已過期。
tool create 驗證失敗檢查順序為:名稱的正規表示式、缺少 description、缺少 parameters.type。你看到的會是最先發現的那一個錯誤。見上方推送 tool。
console 出現警告:"backend declares tools with no registered handler"平台上存在某個 tool(透過 tool create 或 console 編輯器推送的),但你的 tools 對應表裡沒有相符的 handler 鍵。補上缺少的 handler,或移除那個用不到的 tool 定義。
發了一把新金鑰之後全部壞掉這是預期行為——發放新金鑰會立刻撤銷前一把。請把所有已部署、仍在使用舊金鑰的地方都更新成新的。
某次 tool call 看起來卡住了很可能是某個 kind: query 的 tool,它的 handler 永遠沒有 resolve。query 類型的 tool 在等待期間會佔住後端共用的鎖——請確保 handler 一定會 resolve 或 reject,並且只在真的需要拿回資料時才使用 query。