JavaScript SDK (@asgard-js/core)
@asgard-js/core 是框架無關的 JavaScript SDK,處理 SSE 串流連線、Channel 管理與 Conversation 狀態。適用於 Node.js、瀏覽器,以及不使用 @asgard-js/react 元件的場景(Headless、Vue、Svelte 等)。
若你用 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 設定
| 參數 | 型別 | 必填 | 說明 |
|---|---|---|---|
botProviderEndpoint | string | 是 | Bot Provider 的 base URL |
apiKey | string | 否 | API 金鑰(Direct Connect 模式) |
customHeaders | Record<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 | 何時丟出 |
|---|---|---|
HttpError | isHttpError | HTTP 請求回傳非 2xx |
ChannelBusyError | isChannelBusyError | 該 channel 上已有一個 run 在執行中 |
ChannelAwaitingConsentError | isChannelAwaitingConsentError | channel 停在工具呼叫的同意提示上 |
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_SIZE | 1000 | 單次請求的筆數上限 |
SOURCE_SET_DEFAULT_MAX_ENTRIES | 10000 | 預設最多取回的筆數 |
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$(RxJSObservable,只在該切片變動時發射),以及對應的快照 gettergetTasks()/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 時,直接用 @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
延伸閱讀
- 前端 SDK 使用指南 — 模式選型與完整整合說明
- React SDK 參考文件 —
@asgard-js/react元件說明