呼叫介面
本頁介紹 Node.js SDK 的引數、返回值、錯誤、重試、分頁、檔案和測試。安裝與客戶端配置見Node.js SDK,每個服務的方法和 REST 文件見服務與方法。下文的 im 是已建立的 DRClient。
| 要做什麼 | 看哪裡 |
|---|---|
| 寫引數、讀響應、保留大整數原文 | 命名規則 |
| 給單次呼叫設定超時、取消、請求 ID | 每次呼叫的選項 |
| 判斷錯誤 | 錯誤處理 |
| 瞭解哪些請求會自動重試、處理限流 | 重試與冪等 |
| 翻頁、遍歷全部資料 | 自動翻頁 |
| 讀取批次介面的逐項結果、批次匯入 | 批次結果與超過上限的批次 |
| 上傳、下載檔案,傳送檔案訊息 | 檔案上傳與下載 |
| 日誌、指標和單元測試 | 日誌與排查、測試自己的程式碼 |
| 獨立 RTC 頻道、AI 流式回覆 | 使用獨立頻道、AI 流式回覆 |
命名規則
- 服務和方法使用 camelCase,如
im.users.issueLoginTicket()、im.groups.listMembers()。 - 請求和響應的資料欄位保持 REST 的 snake_case,如
auto_create、client_msg_id、next_cursor。 - 路徑引數是位置引數,查詢引數或請求體放在引數物件中,呼叫選項總是最後一個引數。
undefined表示不傳,null表示顯式傳 null;能否清空欄位以對應 REST 介面為準。未知引數會報invalid_params。- ID 和時間戳按服務端的資料格式返回,不自動把 RFC 3339 字串改為 Date。
await im.users.get('alice');
await im.users.update('alice', { nickname: '小明' });
await im.users.list({ limit: 20, username_prefix: 'a' });
await im.users.get('alice', { timeoutMs: 3000 });資料型別從主包以 import type 匯入,編輯器可以補全引數和響應。響應保留未知欄位;完整原始響應可通過主包匯出的 RAW 符號讀取:result[RAW],它不參與普通列舉和 JSON.stringify。
只有呼叫選項時,也要保留可選引數物件的位置。例如 im.users.list(undefined, { timeoutMs: 3000 }),不能把 timeoutMs 當作查詢欄位放進第一個引數。
原始 JSON
需要保留訊息正文或擴充套件欄位的原文時,使用 RawJSON,響應中的 body_raw 等欄位也使用它。支援的欄位以方法型別為準。
import { RawJSON } from '@deeprespond/im-server-sdk';
await im.messages.send({
from: 'notice',
to_user: 'alice',
type: 'custom',
body: new RawJSON('{"order_id":9007199254740993}'),
});RawJSON 在構造時檢查 JSON;SDK 編碼請求時保留原文。普通 JavaScript number 不能精確表示超過安全整數範圍的整數,先 JSON.parse 再包裝不能恢復精度。
每次呼叫的選項
| 欄位 | 說明 |
|---|---|
timeoutMs | 每次嘗試的時限(毫秒),覆蓋客戶端設定,也覆蓋完成上傳的預設 150 秒 |
totalTimeoutMs | 整次呼叫的總時限(毫秒),含重試、限流等待和該呼叫自己執行的令牌換取 |
signal | AbortSignal,取消正在進行的請求及等待 |
maxRetries | 可安全重試的請求在網路錯誤、超時、5xx 後的重試次數 |
maxRateLimitedRetries | 被限流後等待重發的次數 |
maxRetryAfterMs | 自動等待 Retry-After 的上限(毫秒) |
requestId | X-Request-ID,1~64 個字母、數字或 ._- |
const user = await im.users.get('alice', {
timeoutMs: 3000,
totalTimeoutMs: 5000,
signal: AbortSignal.timeout(5000),
requestId: 'profile-alice-1',
});AbortSignal.timeout() 觸發時錯誤碼為 timeout,手動 abort 為 canceled。取消或超時的寫操作可能已經在服務端執行,重發前按介面的冪等規則處理。
錯誤處理
服務端錯誤和 SDK 本地錯誤使用 DRError,用 isDRError() 判斷;不要依賴錯誤提示文本。
import { isDRError } from '@deeprespond/im-server-sdk';
try {
await im.users.get('alice');
} catch (error) {
if (isDRError(error)) {
console.error({ code: error.code, reason: error.reason, requestId: error.requestId });
if (error.code === 'not_found') {
// 根据业务决定是否创建用户。
}
}
throw error;
}| 屬性 | 含義 |
|---|---|
code、reason、details | 錯誤碼、details.reason、附加資訊 |
status | HTTP 狀態碼,本地失敗時可能沒有 |
serverMessage | 服務端返回的提示文本 |
requestId、op | 請求 ID 和介面名,例如 users.issue_login_ticket |
retryAfter | 服務端要求等待的時間,單位為秒 |
attempts、retried | 嘗試次數;錯誤是否來自重試的嘗試 |
partial | 分批、遍歷、下載等操作已經完成的部分,形狀按具體操作區分 |
isTemporary | 是否屬於網路、超時、限流或 5xx 等暫時錯誤 |
isPermanentlyUnavailable | 是否因應用刪除或租戶登出永久不可用 |
本地常見錯誤包括 invalid_params、network_error、timeout、canceled、invalid_response、client_unavailable。業務錯誤見錯誤碼。鉤子和使用者處理函式丟擲的異常也可能原樣傳出。
isServiceUnavailable 可識別應用或租戶暫時不可用,以及因此進入冷卻期的 client_unavailable。排查時保留 op 和 requestId,具體處理見鑑權與網路。
重試與冪等
方法參考標出每個 REST 呼叫的自動重試類別:
| 類別 | 網路錯誤、超時或 5xx 後的行為 |
|---|---|
| 查詢 | 自動重試 |
| 冪等 | 自動重試,例如用固定目標設定資料、解除禁言 |
| 唯一建立 | 自動重試;再次返回 already_exists 時結合 retried 查詢並核對已有物件 |
| 不重試 | 不自動重試,避免重複產生副作用 |
傳送訊息預設的 client_msg_id 只在一次 SDK 呼叫內保持穩定。需要跨業務重試去重時,由業務傳入穩定 ID。not_found、already_exists、version_conflict 出現在重試中時,可能是前一次操作已執行,請核對服務端狀態。
限流
429 rate_limited 按 Retry-After 等待,受 maxRateLimitedRetries 和 maxRetryAfterMs 控制;此規則也適用於“不重試”類別的介面。超過等待上限或總時限時不繼續等待,將錯誤交給呼叫方。
rateLimit: { perSecond: 50, burst: 50 } 可在客戶端本地限速;它只約束本程序中共用該客戶端的呼叫,不替代服務端的應用級額度。
自動翻頁
for await (const user of im.users.iter({ username_prefix: 'a', limit: 100 })) {
console.log(user.username);
// 提前 break 不会继续请求下一页。
}迭代器逐頁請求,用到下一頁時才繼續。遍歷選項支援 CallOptions 中除 totalTimeoutMs 外的欄位;限制整個遍歷使用 signal。某頁最終失敗時丟擲錯誤,partial 中保留該頁使用的游標,便於用列表方法繼續。
手動翻頁時讀取 items、next_cursor;帶版本號的列表還有 version 和 not_modified。會話訊息和訊息匯出使用各自的序號或訊息 ID 分頁,見查詢訊息。
let cursor: string | undefined;
do {
const page = await im.users.list({ limit: 100, cursor });
for (const user of page.items) console.log(user.username);
cursor = page.next_cursor ?? undefined;
} while (cursor !== undefined);游標按服務端原樣傳回,不解析或自行遞增。對帶版本號的增量列表,not_modified 表示版本未變化,不能把此時的空 items 當作刪除本地全部記錄。
批次結果與超過上限的批次
批次請求 HTTP 成功不代表每一項成功。結果提供 ok、failed_items、throwIfFailed():
const result = await im.users.batchCreate([
{ username: 'alice' },
{ username: 'bob' },
]);
if (!result.ok) console.error(result.failed_items);
result.throwIfFailed(); // 有失败项时抛出 BatchError。帶 All 字尾的方法自動按介面上限拆批,如 batchCreateAll()。失敗項保留在結果中;整個請求失敗時中斷並丟擲錯誤,partial 保留已完成部分。全部方法見使用者服務等對應章節。
import { BatchError } from '@deeprespond/im-server-sdk';
const result = await im.users.batchCreateAll([
{ username: 'import_alice' },
{ username: 'import_bob' },
]);
try {
result.throwIfFailed();
} catch (error) {
if (!(error instanceof BatchError)) throw error;
for (const item of result.failed_items) {
console.error(item.key, item.code);
}
}業務重試優先針對失敗項;不要在部分成功後無條件重跑整個匯入任務。批次方法丟擲的請求級 DRError 與 throwIfFailed() 丟擲的逐項 BatchError 分開處理。
檔案上傳與下載
const file = await im.media.upload('/data/report.pdf', {
purpose: 'attachment',
kind: 'file',
onProgress(uploaded, total) {
console.log(uploaded, total);
},
});
await im.media.download(file.url, '/data/report-copy.pdf');上傳來源支援路徑、file: URL、Buffer、Uint8Array、ArrayBuffer、Blob。路徑預設提供檔名;記憶體來源上傳普通檔案時傳 name。不支援 Readable 或非同步迭代器來源,需要重讀分片時請先儲存為檔案。
SDK 自動建立上傳、傳輸和完成上傳,大檔案自動分片,concurrency 預設 4。onProgress(uploaded, total) 在分片完成時回撥,一次上傳只在完成時回撥。群檔案需要 group_id,使用者頭像需要 owner,詳見上傳檔案。
已儲存上傳會話的 file_id 時,可用 im.media.resumeUpload(fileId, source, params?, options?) 恢復;會話必須仍有效,來源應與原檔案一致。取消上傳會嘗試撤銷上傳會話。
下載第一個引數是媒體檔案的 url,SDK 先換取授權下載地址;目標支援路徑、file: URL、Writable 或 WritableStream,返回寫入的位元組數。路徑下載成功後才替換目標檔案,失敗時刪除臨時檔案。響應體按空閒時限讀取,整體時長可用 signal 或 totalTimeoutMs 限制。
上傳為附件後,可以將穩定的媒體 URL 放入檔案訊息,完整的檔案欄位見訊息體格式:
const file = await im.media.upload(Buffer.from('Node SDK 示例附件\n'), {
purpose: 'attachment',
kind: 'file',
name: 'hello.txt',
content_type: 'text/plain',
});
await im.messages.send({
from: 'alice',
to_user: 'bob',
client_msg_id: 'document-hello-1',
type: 'file',
body: { url: file.url, name: file.name ?? 'hello.txt', size: file.size },
});file.url 用於訊息和後續下載,臨時 download_url 不作為訊息中的長期地址。向瀏覽器轉發檔案時,由業務 HTTP 服務設定適當的響應型別和下載頭。
內容安全複審
const count = await im.moderation.processPending(async (item, content, ctx) => {
if (item.scene !== 'message') return null; // 留给人工处理。
// 按自己的规则检查 content;业务 I/O 可使用 ctx.signal。
return { decision: 'no_violation', note: '业务复审通过' };
}, { scene: 'message', limit: 20 });返回 null 或 undefined 表示跳過。SDK 帶記錄版本提交結論,版本衝突時跳過;stopOnError 預設 false,處理函數出錯時記日誌並繼續,設為 true 則丟擲。返回提交結論的條數,介面規則見稽核記錄。
日誌與排查
const im = new DRClient({
baseUrl: 'https://api.example.com',
orgName: process.env.IM_ORG_NAME!,
appName: process.env.IM_APP_NAME!,
clientId: process.env.IM_CLIENT_ID!,
clientSecret: process.env.IM_CLIENT_SECRET!,
logger: { level: 'warn', sink: entry => console.warn(entry) },
hooks: {
beforeRequest(req) { req.headers.set('X-Trace-ID', 'order-service'); },
afterResponse(info) { console.log(info.op, info.status, info.durationMs, info.willRetry); },
},
});本例的 DRClient 從主包匯入。鉤子在每次嘗試前後 await 執行,可用於追蹤和指標;SDK 會重新設定 Authorization、X-Request-ID、Content-Type。不要在鉤子裡列印憑據、請求正文或完整簽名地址。
im.stats() 返回以介面名為鍵的計數,包含請求次數、最終失敗數、重試數和限流等待時長。介面名仍使用 snake_case,與方法名的 camelCase 區分,例如 users.issue_login_ticket。
測試自己的程式碼
testing 子路徑可與 node:test、Vitest 或 Jest 一起使用,不需要真實服務端。
import assert from 'node:assert/strict';
import { FakeOpenAPI } from '@deeprespond/im-server-sdk/testing';
const fake = new FakeOpenAPI();
fake.respond('messages.send', {
status: 201,
json: { message_id: 'm1', client_msg_id: 'order-1', created_at: '2026-10-08T08:00:00Z' },
});
const im = fake.client();
try {
const result = await im.messages.send({
from: 'notice', to_user: 'alice', client_msg_id: 'order-1',
type: 'text', body: { text: '你好' },
});
assert.equal(result.message_id, 'm1');
assert.equal(fake.calls('messages.send').length, 1);
} finally {
await im.close();
}| 工具 | 用法 |
|---|---|
fake.respond(op, { json, status?, headers?, times? }) | 設定響應,預設持續有效 |
fake.fail(op, code, { status?, retryAfter?, times? }) | 模擬錯誤,預設一次;retryAfter 為秒 |
fake.handle(op, responder) | 自己處理 Request 並返回 Response |
fake.calls(op)、fake.requests | 檢視已記錄的呼叫 |
fake.revokeTokens() | 讓已經換取的令牌失效 |
op 使用方法參考中的介面名,不是 JavaScript 方法名。測試回撥見本地測試。
使用獨立頻道
const created = await im.channels.create('meeting-demo', {
name: '项目会议', media: 'video', access_mode: 'ticket',
});
const issued = await im.channels.issueTickets(created.channel.channel_id, {
users: [{ username: 'alice', role: 'publisher' }],
});
// 仅把 issued.tickets 交给绑定用户,不输出原文。
const page = await im.channels.list({ status: 'open', limit: 20 });
const corrections = await im.channels.usageAdjustments({ limit: 20 });- 引數和結果是各自的
RTCChannel…型別,用WithRaw保留原始資料。 - 頻道列表沒有
iter方法,把next_cursor傳給下一次呼叫自己翻頁。 expires_at不傳表示不修改,傳null表示清除。- 建立結果中的
created表示服務端返回 201(新建),關閉和移出結果中的pending表示返回 202(處理中)。 - 簽發票據不自動重試;移出成員和撤銷票據重試時保持同一個
operation_id。 - 這些方法從 npm 1.0.0 起提供,對應的介面見頻道方法。
修改頻道前讀取當前版本。expires_at: null 表示清除到期時間,省略該欄位則保留原值:
const current = await im.channels.get('meeting-demo');
const updated = await im.channels.update('meeting-demo', {
version: current.channel.version,
name: '项目例会',
expires_at: null,
});
console.log(updated.channel.name, updated.channel.version);version_conflict 時重新讀取並核對業務意圖。頻道 ID、封禁記錄版本和 operation_id 各有用途,不互相替代;簽發結果只發給票據繫結的使用者,客戶端使用自己的登入身份加入媒體。
遊戲房間
遊戲房間給多人線上遊戲提供即時對戰通道。你的服務端用 im.game 建房、給玩家簽發加入票據、對局結束時結束房間;玩家加入房間和對戰由客戶端 SDK 完成。下面是一局對戰在服務端要做的事:
import { DRClient, isDRError } from '@deeprespond/im-server-sdk';
const im = new DRClient({
baseUrl: process.env.IM_API!, orgName: 'demo-org', appName: 'demo',
clientId: process.env.IM_CLIENT_ID!, clientSecret: process.env.IM_CLIENT_SECRET!,
});
/** 匹配成功后建房,给每个玩家签一张加入票据。 */
export async function startMatch(matchId: string, players: string[]): Promise<Record<string, string>> {
try {
// 房间 ID 用你的对局 ID:重试或重复调用都只有一个房间
await im.game.createRoom(matchId, { max_players: players.length, start_mode: 'all_ready', ext: { mode: 'ranked' } });
} catch (err) {
if (isDRError(err) && err.reason === 'game_capacity_unavailable') throw new Error('游戏服务繁忙,请稍后再试');
if (isDRError(err) && err.reason === 'open_rooms') throw new Error('同时进行的对局太多');
throw err;
}
// 不会自动重试;超时后直接再签一次,旧票据随之作废
const tickets = await im.game.issueTickets(matchId, { users: players.map((username, seat) => ({ username, seat })) });
// 经你的业务接口分别交给每个玩家,不要写进日志
return Object.fromEntries(tickets.map((t) => [t.username, t.ticket]));
}
/** 对局结束时带上结算结果结束房间。 */
export async function finishMatch(matchId: string, winner: string): Promise<void> {
const { accepted } = await im.game.closeRoom(matchId, { reason: '对局结束', result: { winner } });
if (!accepted) console.info(`${matchId} 之前已在结束,这次的结果不生效`);
}
/** 把作弊的玩家移出房间。 */
export async function kickCheater(matchId: string, username: string): Promise<boolean> {
const { removed } = await im.game.removeMember(matchId, username, { reason: '作弊' });
return removed; // false:他已经不在房间里
}| 要做什麼 | 方法 | 注意 |
|---|---|---|
| 建房 | createRoom | 房間 ID 用你的對局 ID。同樣的引數重複呼叫返回同一個房間(created 為假),網路錯誤時自動重試 |
| 簽發加入票據 | issueTickets | 每次 1–10 人。不自動重試:超時後直接再籤一次,同一使用者之前沒用的票據隨之作廢。票據原文只返回這一次,不要寫進日誌 |
| 手動開局 | startRoom | 只用於開局方式為 manual 的房間 |
| 結束房間 | closeRoom | 結算結果原樣發給玩家和 game_room.closed 回撥。房間之前已在結束時 accepted 為假,這次的原因和結果不生效 |
| 移出玩家 | removeMember | SDK 自動生成 operation_id,重試時不變。玩家不在房間裡時 removed 為假 |
| 查詢 | getRoom、listRooms / iterRooms、listMembers、listRegions | 列表按建立時間從新到舊;狀態可以寫多個,用逗號分隔 |
- 引數和結果是
CreateGameRoomParams、GameRoom、GameTicket等型別,從主包匯入;結果用WithRaw保留原始資料。 created、accepted、removed由 HTTP 狀態碼得出,不是響應體中的欄位。- 常見錯誤:大區暫時沒有空閒的遊戲節點為
unavailable(game_capacity_unavailable),稍後重試,或在建房時給出候選大區;同時進行的房間數達到上限為limit_exceeded(open_rooms)。全部錯誤見遊戲房間的錯誤。 - 擴充套件欄位
ext不超過 2KB,結算結果result不超過 4KB;上限和計費見控制台、計費與限制。 - 對局結束後的結算回撥(
game_room.closed)見遊戲房間回撥。 - 每個方法的簽名和對應的介面見遊戲房間方法,請求和返回的欄位見遊戲房間:服務端介面。遊戲房間的方法從 npm 1.0.7 起提供。
AI 流式回覆
AI 對話中,使用者給機器人發訊息後你會收到 ai.message_received 回撥,然後用 im.ai.startStream 開始一條回覆,邊生成邊寫入。返回的 Stream 替你處理增量的編號與合批(滿 20 毫秒或 4 KB 發一段)、持續寫入與按批寫入的切換、失敗重發;連續三次寫入失敗時以 fail('transport') 結束。機器人管理用 im.ai.createBot 等方法,見服務與方法。
收到訊息後開始回覆
回撥處理函式應儘快返回,模型呼叫不必等待。client_stream_id 用事件 ID,回撥重新投遞時不會開始第二條回覆:
import { CallbackHandler, MemoryDedupStore, MemoryNonceStore } from '@deeprespond/im-server-sdk/callback';
const handler = new CallbackHandler({
secrets: { [ENDPOINT_ID]: [SECRET] },
nonceStore: new MemoryNonceStore(),
dedupStore: new MemoryDedupStore(),
}).onAIMessage(async (event, data) => {
const stream = await im.ai.startStream({
bot: data.bot,
to: data.from,
reply_to_seq: Number(data.message['seq']),
client_stream_id: event.id,
});
const question = String((data.message['body'] as { text?: string } | undefined)?.text ?? '');
void reply(stream, question);
});startStream 的 replace: true 先結束會話中正在生成的回覆;batch_only: true 只用按批寫入(執行環境不支援流式請求體時,如部分雲函式)。
對接模型
pipeOpenAI、pipeOpenAIResponses、pipeAnthropic、pipeGemini 讀完模型的流後自動 finish(帶上結束原因、模型名和用量);模型返回錯誤時以 fail('model_error', 说明) 結束並丟擲這個錯誤。引數可以是官方 SDK(openai、@anthropic-ai/sdk、@google/genai)的流,也可以是 SSE 格式的原始響應(Response、ReadableStream,或產出字串、位元組的迭代器)。相容 OpenAI 格式的模型(DeepSeek、通義千問、Kimi、豆包、vLLM、Ollama 等)都用 pipeOpenAI:
import OpenAI from 'openai';
import type { Stream } from '@deeprespond/im-server-sdk';
const openai = new OpenAI();
async function reply(stream: Stream, question: string): Promise<void> {
await using _ = stream; // 离开作用域时没有结束的以 fail('canceled') 结束
const completion = await openai.chat.completions.create(
{ model: 'gpt-4.1', messages: [{ role: 'user', content: question }], stream: true, stream_options: { include_usage: true } },
{ signal: stream.signal },
);
await stream.pipeOpenAI(completion);
}四種轉換的規則與 Go SDK 相同。
手動寫入
stream.writeReasoning('先查询天气。');
stream.writeTool('call_1', 'weather', '查询天气', 'running');
// … 调用工具 …
stream.writeTool('call_1', 'weather', '查询天气', 'done');
stream.writeText('明天上海**多云**,');
stream.writeText('最高 23℃。');
stream.writeImage({ url: 'https://cdn.example.com/a.png', width: 800, height: 600 });
const result = await stream.finish({ model: 'gpt-4.1', usage: { input_tokens: 120, output_tokens: 380 } });write*只放入記憶體,不需要await;與上一段同類型的文字追加到同一個片段。flush()立即發出並等待確認;finish()先確認全部內容再結束。- 模型呼叫失敗時呼叫
stream.fail('model_error', '模型暂时不可用'),已生成的內容保留,使用者看到“生成中斷”和這句說明。
停止生成
使用者點停止生成、回覆超時或被新的回覆替換時,stream.signal(AbortSignal)被觸發,signal.reason 為 stream_closed 的 DRError;之後的寫入丟擲這個錯誤。把 stream.signal 傳給模型請求,模型呼叫隨之中止;pipe* 也會停止讀取並中止模型的流(官方 SDK 的流呼叫 controller.abort())。
CallbackHandler 收到 ai.stream_stopped 時自動停止本程序中對應的流,不需要註冊處理函式;要額外處理時用 onAIStreamStopped。自己處理回撥(如從佇列中取出事件)時呼叫 im.ai.streamStopped(streamId)。
