调用接口
本页介绍 Node.js SDK 的参数、返回值、错误、重试、分页、文件和测试。安装与客户端配置见Node.js SDK,每个服务的方法和 REST 文档见服务与方法。下文的 im 是已创建的 DRClient。
命名规则
- 服务和方法使用 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。
原始 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。业务错误见错误码。钩子和用户处理函数抛出的异常也可能原样传出。
重试与幂等
方法参考标出每个 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 分页,见查询消息。
批量结果与超过上限的批量
批量请求 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 保留已完成部分。全部方法见用户服务等对应章节。
文件上传与下载
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 限制。
内容安全复审
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 原始数据;分页 next_cursor 手动传给下一次调用,没有频道 iter 方法。expires_at 不传为不改、null 为清除;created / pending 包装分别取自 201 / 202。签发不自动重试;移出和撤票保持 operation_id。参见频道方法。本轮 API 在源码中,尚未发布新制品。
