跳至主要内容
@asgard-js/core JavaScript SDK 參考文件

JavaScript SDK (@asgard-js/core)

@asgard-js/core 是框架無關的 JavaScript SDK,處理 SSE 串流連線、Channel 管理與 Conversation 狀態。適用於 Node.js、瀏覽器,以及不使用 @asgard-js/react 元件的場景(Headless、Vue、Svelte 等)。

使用 React?

若你用 React,通常不需要直接使用 @asgard-js/core。改用 @asgard-js/react 的 <Chatbot> 元件即可,詳見 React SDK 參考文件。

安裝​

npm install @asgard-js/core

核心概念​

類別說明
AsgardServiceClient連線設定(botProviderEndpoint、apiKey、customHeaders)
Channel一個對話頻道,透過 SSE 與 Bot Provider 通訊
Conversation儲存訊息狀態的容器

基本使用​

import { AsgardServiceClient, Channel, Conversation } from '@asgard-js/core';

const client = new AsgardServiceClient({
botProviderEndpoint:
'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
});

const conversation = new Conversation({ messages: new Map() });

const channel = await Channel.reset({
client,
customChannelId: 'channel-123',
conversation,
statesObserver: (states) => {
console.log('isConnecting:', states.isConnecting);
console.log(
'Messages:',
Array.from(states.conversation.messages.values()),
);
},
});

await channel.sendMessage({ text: '你好!' });

AsgardServiceClient 設定​

參數型別必填說明
botProviderEndpointstring是Bot Provider 的 base URL
apiKeystring否API 金鑰(Direct Connect 模式)
customHeadersRecord<string, string>否自訂 HTTP header

通道與檔案操作​

AsgardServiceClient 除了建立連線之外,還提供這些方法:

方法簽章說明
uploadFile(file: File, customChannelId: string) => Promise<BlobUploadResponse>上傳檔案,回傳的 blob 資訊可以帶進後續訊息
downloadChannelHomeFile(relativePath: string, customChannelId: string) => Promise<ChannelHomeDownloadResult>下載該 channel home 底下的檔案
channelMetadata(customChannelId: string) => Promise<ChannelMetadata | null>查詢 channel 是否存在,不存在回傳 null
suspendChannel(customChannelId: string, options?) => Promise<void>中止該 channel 上正在執行的 run
deleteChannel(customChannelId: string) => Promise<void>刪除 channel
detach(options: { timeoutMs: number }) => void停止接受新請求,等進行中的請求結束後才真正關閉
close() => void立即關閉

detach 和 close 的差別在於進行中的請求。close 直接關掉;detach 會標記為不再接受新請求,若當下沒有進行中的請求就立即關閉,否則等它們結束,適合頁面離開前的收尾。

這些方法都需要設定 botProviderEndpoint,因為各自的端點是從它推導出來的。沒設定時會丟出說明是哪個端點推導失敗的錯誤。

訊息回饋​

使用者對某一則回覆按讚或倒讚,透過 sendMessageFeedback 送出:

import {
FEEDBACK_COMMENT_MAX_BYTES,
feedbackCommentByteLength,
composeFeedbackMessage,
} from '@asgard-js/core';

await client.sendMessageFeedback({
customChannelId,
messageId, // 被評價的那則回覆的 messageId
verdict: 'BAD', // 'GOOD' | 'BAD'
comment: '答案跟我問的不是同一件事',
});

回傳的 messageId 是這筆回饋自己的 id,和被評價的那則回覆不同,另外還有 transcript 的 seq,重新加入 channel 時會在該位置重播成 asgard.message.feedback 事件。

留言長度上限是 8 KiB 而不是 8192 字元​

FEEDBACK_COMMENT_MAX_BYTES 是 8 * 1024,單位是 UTF-8 位元組。中文一個字佔三個位元組,所以用字元數檢查會讓一則伺服器會用 400 拒絕的留言通過。要用 feedbackCommentByteLength(comment) 量:

if (feedbackCommentByteLength(comment) > FEEDBACK_COMMENT_MAX_BYTES) {
// 提示使用者縮短
}

把回饋一併說給 AI 聽​

composeFeedbackMessage(verdict, comment?) 組出要送進對話的後續訊息:開頭是該 verdict 對應的前綴,使用者有留言時空一行後接原文。

composeFeedbackMessage('BAD', '答案跟我問的不是同一件事');
// "[Response Feedback: Bad]\n\n答案跟我問的不是同一件事"

RESPONSE_FEEDBACK_PREFIX 是那兩個前綴字串([Response Feedback: Good] / [Response Feedback: Bad])。它們是逐位元組的平台約定:後端的 system prompt 認得的就是這兩個字串,據此讓 agent 把這則訊息當成對前一則回覆的插話,簡短回應、必要時調整,然後繼續對話。不要自己改寫這段前綴。

錯誤處理​

@asgard-js/core 匯出三個錯誤類別與各自的 type guard。它們都是 Error 的子類別,用 instanceof 或 guard 判斷皆可。

類別Type guard何時丟出
HttpErrorisHttpErrorHTTP 請求回傳非 2xx
ChannelBusyErrorisChannelBusyError該 channel 上已有一個 run 在執行中
ChannelAwaitingConsentErrorisChannelAwaitingConsentErrorchannel 停在工具呼叫的同意提示上
import {
isHttpError,
isChannelBusyError,
isChannelAwaitingConsentError,
} from '@asgard-js/core';

try {
await channel.sendMessage({ text: input });
} catch (error) {
if (isChannelAwaitingConsentError(error)) {
// 先回答同意提示,這一輪才送得出去
} else if (isChannelBusyError(error)) {
// 上一輪還沒結束,例如把送出按鈕維持在停用狀態
} else if (isHttpError(error) && error.status === 429) {
// 依 error.body 判斷是額度用完還是速率限制
}
}

HttpError​

帶著 status、statusText 與 body 三個唯讀屬性。body 是解析後的錯誤內容,實務上是分辨同一個狀態碼底下不同原因的依據,例如以 body 裡的 reason_code 區分 429 是額度用盡還是單純的速率限制。

body 有可能是 undefined:回應不一定帶得動 JSON,解析失敗是正常結果而不是呼叫端的錯,所以讀之前要先確認。

ChannelBusyError​

Channel.sendMessage() 在該 channel 已有 run 執行中時會 reject 這個錯誤,屬性 runKind 說明是哪一種 run 佔著。

run 在伺服器端是背景執行的,再送一個等於讓兩個 run 同時寫同一份對話記錄,所以這裡是明確拒絕而不是靜默丟棄,呼叫端才知道訊息根本沒送出去。

SDK 內建的輸入框已經自己擋掉這種情況,所以這個錯誤實際上只會出現在你用程式送訊息的路徑上。

ChannelAwaitingConsentError​

Channel.sendMessage() 與 Channel.nudge() 在 channel 停在工具呼叫同意提示時會 reject 這個錯誤。伺服器把這種 channel 保持在暫停狀態,只有回覆同意才能繼續,其他任何一輪都會被拒絕,nudge 也一樣。

這個暫停狀態偵測不到「run 執行中」:同意提示是在 run 結束之後才送達的,所以提示出現在畫面上時 run 早就結束,channel 看起來是閒置的。先用 Channel.replyToolCallConsents() 回答,再送下一輪。

屬性 processId 是暫停批次的 process id,但重新加入既有 channel 時它會是空字串。重新載入後恢復的提示由後端從持久化的暫停狀態重建,那條路徑刻意不填 process id,因為判斷是否處於同意暫停要看提示存不存在,不是看這個 id。把它當診斷線索用,不要當 key。

Sandbox 檔案系統​

Agent 執行時掛載的 Sandbox 有一組檔案操作 API,全部以 sandboxName 加路徑定位:

方法簽章
sandboxFsList(sandboxName, path) => Promise<SandboxFsListResult>
sandboxFsStat(sandboxName, path) => Promise<SandboxFsStatResult>
sandboxFsRead(sandboxName, path, options?) => Promise<SandboxFsReadResult>
sandboxFsWrite(sandboxName, path, ...) => Promise<void>
sandboxFsMkdir(sandboxName, path) => Promise<void>
sandboxFsRemove(sandboxName, path) => Promise<void>
sandboxFsRemoveAll(sandboxName, path) => Promise<void>
sandboxFsCopy(sandboxName, src, dst, ...) => Promise<void>
sandboxFsMove(sandboxName, src, dst, ...) => Promise<void>
sandboxFsWatch(sandboxName, path) => Observable<SandboxFsWatchEvent>

sandboxFsWatch 回傳的是 RxJS Observable 而不是 Promise,訂閱後持續收到該路徑的變動事件,不用時要自行 unsubscribe。

generateSandboxBrowserOpenUrl(sandboxName) 產生可在瀏覽器開啟該 Sandbox 的網址。

resolveSandboxUri(uri) 把 Sandbox URI 解析成 SandboxUriIntent,無法解析時回傳 null。reconcileLaunched(metadata) 整理已啟動的 Sandbox 清單。

React 端不必直接呼叫這些方法,createSandboxFsProviders 會把它們包成檔案總管需要的形狀,詳見 React SDK。

SourceSet​

AsgardSourceSetClient 存取 SourceSet 的內容。相關常數:

常數值說明
SOURCE_SET_MAX_PAGE_SIZE1000單次請求的筆數上限
SOURCE_SET_DEFAULT_MAX_ENTRIES10000預設最多取回的筆數
SOURCE_SET_VOLUME_ROOT''volume 根路徑,是空字串而不是 /

assertVolumePath(path, options?) 驗證路徑合法,不合法時丟出錯誤。根路徑是空字串這點容易寫錯,用這個函式檢查而不要自己比對。

事件與衍生資料​

fetchSse(payload, options?) 建立 SSE 連線,回傳 RxJS Subscription。rejoinSse(customChannelId, options?) 重新加入既有 channel 並重播 transcript。handleEvent(response) 把單一 SSE 事件餵進 client 的內部狀態,通常由 SDK 自己呼叫。

從事件推導任務與子代理的工具:

函式用途
isTaskTool(call) / isAgentTool(call)判斷一次工具呼叫是否為任務 / 子代理
isSubagentChildTool(parentToolUseId?)判斷是否為子代理底下的工具呼叫
reduceTaskEvents(events)把 TaskToolEvent[] 收斂成 Task[]
reduceSubagents(events)把 SubagentEvent[] 收斂成 Subagent[]
conversationToSubagentEvents(messages)從對話訊息萃取子代理事件
tasksEqual(a, b) / subagentsEqual(a, b)比較兩份清單是否相同,用於避免不必要的重繪

Headless 衍生狀態(Task / Subagent / 標題)​

0.3.x 起,@asgard-js/core 額外提供一組框架無關的衍生狀態 API,讓 Headless / Vue / Svelte 等消費端能在 <Chatbot> 之外自行渲染任務清單、子代理清單與對話標題:

  • Channel 上的響應式 store:channel.tasks$、channel.subagents$、channel.channelTitle$(RxJS Observable,只在該切片變動時發射),以及對應的快照 getter getTasks() / getSubagents() / getChannelTitle()。
  • 從 conversation$ 自行建立:createDerivedStores(conversation$) 回傳 { tasks$, subagents$, getTasks, getSubagents, teardown };或用 deriveTasks(conversation) / deriveSubagents(conversation) 做一次性推導。
  • 加入既有 channel(不清空歷史):Channel.restore(config, options?) 會先透過 client.channelMetadata()(GET /channel/metadata)判斷 channel 是否存在,存在則重播 server transcript(client.rejoinSse())而非送出 RESET_CHANNEL。
React 消費端

使用 React 時,直接用 @asgard-js/react 的 useTaskList / useSubagents / useChannelTitle hooks,它們已把上述 store 橋接進 useSyncExternalStore。

Sandbox 瀏覽器​

Sandbox 裡跑的瀏覽器可以嵌進你自己的畫面直接操作,不必另開分頁。

createSandboxBrowserSession(sandboxName) 向 POST {botProviderEndpoint}/sandbox/{sandbox_name}/browser/session 取得一組串流憑證,回傳 { wsUrl, token }。拿到之後交給 createSandboxBrowserTransport 建立連線,畫面與輸入都走這條 WebSocket。

這跟既有的 generateSandboxBrowserOpenUrl(sandboxName) 是兩條路,不是取代關係:後者給你一個一次性網址讓使用者在新分頁開啟,仍然保留作為退路;前者給你一個 socket,由 SDK 在你的頁面裡自己渲染。

遠端桌面協定用的是 X11 keysym,不是瀏覽器的 KeyboardEvent.key,所以 core 也匯出一組轉換工具:

匯出用途
KEYSYM常用按鍵的 keysym 常數表
charToKeysym(char)單一字元轉 keysym
keyToKeysym(key)KeyboardEvent.key 轉 keysym
mapModifierKeysym(...)處理 Shift/Ctrl/Alt 這類修飾鍵
isPrintableKeysym(keysym)判斷是不是可列印字元

滾輪事件各瀏覽器的單位不一致,normalizeWheel(event) 把它正規化;WHEEL_MAX 是單次滾動量的上限,WHEEL_THROTTLE_MS 是送出的節流間隔。遠端游標的影格用 decodeCursorFrame 解碼。

這些低階工具在你用 React SDK 的 SandboxBrowserPanel 時都不必自己碰,詳見 React SDK。

互動式 Demo​

@asgard-js/core 的 Headless 用法與事件處理範例:SDK Demo — Headless

延伸閱讀​