事件与错误处理
Web SDK 用事件通知你数据和状态的变化,用 DRError 报告所有的错误。本页列出全部事件和它们的载荷、错误对象的结构、全部错误码与处理建议,以及 SDK 在离线和出错时自动做了什么。
大多数界面不需要直接处理数据变化的事件:会话列表、消息列表等可订阅的列表会自动更新。事件主要用于三类场景:登录和连接状态的变化(回到登录页、显示“连接中”),提醒(新消息通知、来电),以及读取同步状态的值(未读总数、正在输入等)时得知何时重新读取。
订阅与取消
const off = im.on('message.received', ({ message, alert }) => {
if (alert) showToast(`${message.sender}:新消息`);
});
// 不再需要时取消
off();
// 只处理一次
im.once('sync.completed', ({ trigger }) => console.log('第一次同步完成', trigger));im.on(event, listener)返回取消订阅的函数,可以重复调用;im.once只触发一次。- 事件名和载荷都有类型:
listener的参数类型由事件名决定(DREvents[事件名])。 - 监听函数抛出的异常被记为
warn日志,不影响其他监听函数,也不影响 SDK。 - 数据变化的事件在本地数据更新之后发出,载荷只说明变了什么(如哪些会话变了),你从对应的列表或方法读取最新的数据。
- 同一个变化只发出一次:本人操作的结果和随后服务端的通知会合并,不会收到重复的事件。
- 同一浏览器的每个标签页都会收到同样的事件。
im.destroy()之后不再发出任何事件。
在 React、Vue 中订阅事件,可以用绑定包的 useDREvent,组件卸载时自动取消,见在 React 和 Vue 中使用。
全部事件
登录与连接
| 事件 | 载荷 | 说明 |
|---|---|---|
auth.stateChanged | AuthState | 登录状态变化:登录、退出、会话结束(带原因)、服务暂停与恢复,见会话结束 |
auth.userSwitched | { username: string } | 其他标签页登录了另一个用户,本标签页已切换到新用户。打开着的列表已被清空并释放,请按新用户重新加载界面 |
connection.stateChanged | ConnectionState | 连接状态变化,见连接状态 |
sdk.upgradeRequired | { min_version: string } | SDK 版本低于服务端要求的最低版本,连接已停止,本地数据只读。请提示用户刷新页面或升级 |
tabs.roleChanged | { isLeader: boolean; isAlertTab: boolean } | 本标签页成为或不再是主标签页、负责提醒的标签页 |
tabs.refreshRequired | { reason: 'protocol_version' | 'storage_version' } | 同一浏览器中打开了新版本的页面,或本地数据被新版本升级,本页面只能读取本地数据,请提示用户刷新 |
同步与本地数据
| 事件 | 载荷 | 说明 |
|---|---|---|
sync.completed | { modules: SyncModule[]; trigger: string } | 一轮同步完成。modules 为这一轮同步的模块(config、me、friends、groups、messages、push、calls、chatrooms、presence);trigger 为同步的起因:login(登录后)、reconnect(重连后)、sync_required(服务端要求全量同步)、hint(服务端提示有新数据)、foreground(页面回到前台)、periodic(定期检查)、fallback(长连接不可用时的定期同步) |
sync.failed | { module: SyncModule; error: DRError } | 某个模块同步失败,SDK 会自动重试,一般只需记录日志 |
storage.reset | { reason: 'reset_required' | 'reloaded'; dropped_messages?: number } | 本地数据已重置(服务端要求)或已从本地存储重新加载。打开着的列表已自动重新读取,你只需重新读取自己另外保存的数据;dropped_messages 为因此丢弃的未发出消息数 |
storage.modeChanged | { mode: 'indexeddb' | 'memory'; reason: 'unavailable' | 'quota_exceeded' | 'upgrade_blocked' } | 本地数据改为只保存在内存中:浏览器不允许使用 IndexedDB、磁盘空间不足、或旧版本的页面阻止了升级。功能照常,刷新页面后要从服务端重新加载 |
运行配置与本人
| 事件 | 载荷 | 说明 |
|---|---|---|
config.changed | ClientConfig | null | 应用的运行配置变化,与 im.config 相同;退出后为 null |
me.changed | SelfProfile | null | 本人资料变化,与 im.users.me() 相同;退出后为 null |
me.muteChanged | { scope: 'chat' | 'group' | 'room'; muted: boolean; expires_at: string | null } | 本人被全局禁言或解除禁言(本次连接期间收到的) |
users.changed | { usernames: string[] } | 其他用户的资料缓存更新,用 im.users.get() 显示的界面应重新读取 |
devices.changed | { devices: OnlineDevice[] } | 本人的在线设备变化,与 im.devices.online() 相同 |
devices.newLogin | { session_id: string; device_name: string; platform: string; login_method: string; created_at: string } | 本人在一台新设备上登录,可以提示“你的账号在 XX 上登录” |
会话与消息
| 事件 | 载荷 | 说明 |
|---|---|---|
conversations.changed | { upserted: string[]; removed: string[] } | 会话列表中的会话新增、变化或移除,值为 conversation_key |
conversations.unreadChanged | { total: number; source: 'local' | 'server' } | 未读总数变化,total 与 im.conversations.getUnreadTotal() 相同 |
messages.changed | { conversation_key: string; upserted: number[]; removed: number[]; pending: string[]; renamed_from?: string } | 某个会话的消息变化:upserted、removed 为消息的序号,pending 为发送队列中变化的 client_msg_id;renamed_from 表示这个会话由草稿会话变为正式会话,值为原来的草稿键 |
message.received | { message: MessageView; notify: NotifyDecision | null; alert: boolean } | 收到别人发来的新消息,用于提醒:notify 为是否应该提醒(已考虑免打扰、勿扰时段等),alert 为 true 表示本标签页负责提醒。只在实时收到时发出,补齐的历史消息、重连后同步得到的离线期间的消息不发出 |
message.sendFailed | { client_msg_id: string; conversation_key: string; error: DRError } | 一条消息最终发送失败(自动重试已用完,或被服务端拒绝)。消息留在列表中,状态为 failed,可以让用户重发或删除 |
message.online | { message: OnlineMessage } | 收到只推在线的消息(sendOnline 发出的,如“对方正在操作”的自定义信令),不保存 |
typing.changed | { conversation_key: string; usernames: string[] } | 会话中正在输入的人变化 |
receipts.changed | { conversation_id: string; seqs: number[] } | 群消息的已读人数变化,用 im.messages.getReceipt() 读取 |
联系人与群组
| 事件 | 载荷 | 说明 |
|---|---|---|
friends.changed | { upserted: string[]; removed: string[] } | 好友新增、资料或备注变化、删除,值为用户名 |
blacklist.changed | { upserted: string[]; removed: string[] } | 黑名单变化 |
friendRequests.changed | { lists: Array<'received' | 'sent'>; unread_count: number } | 收到的或发出的好友申请变化,unread_count 为未读的收到的申请数 |
groups.changed | { upserted: string[]; removed: string[] } | 我的群列表或群信息变化,值为群 ID |
groupMembers.changed | { group_id: string; usernames: string[] | null } | 群成员变化;usernames 为 null 表示整个成员列表变化 |
groupRequests.changed | { list: 'pending' | 'invitations' | 'applications'; group_id: string | null } | 入群请求变化:pending 为某个群待审批的请求(带 group_id),invitations 为本人收到的入群邀请,applications 为本人的入群申请 |
推送设置与在线状态
| 事件 | 载荷 | 说明 |
|---|---|---|
push.changed | { settings: boolean; mutes: Array<{ conversation_type: string; target: string }> } | 推送设置(settings 为 true)或会话免打扰变化 |
presence.changed | { items: PresenceState[]; reset: boolean; cleared?: string[] } | 订阅的人在线状态变化;reset 为 true 表示本地的在线状态已全部清除(如重新连接后),cleared 为被清除了状态的人 |
presence.subscribeFailed | { usernames: string[]; reason: 'denied' | 'limit_exceeded' | 'presence_disabled' } | 订阅没有生效的人:没有权限查看、超出订阅上限、应用没有开启在线状态 |
音视频
| 事件 | 载荷 | 说明 |
|---|---|---|
call.incoming | { call: CallHandle; alert: boolean } | 来电。alert 为 true 时本标签页负责振铃和显示来电界面 |
call.changed | { call: CallHandle; change: CallChange | null } | 通话的状态、成员等变化;change 为 null 表示由本标签页自己的操作或重新同步引起 |
groupCall.changed | GroupCallBanner | null | 群中正在进行的通话变化,用于显示“群通话进行中”的横幅(group_id、call_id、media、status、joined_count、initiator、version);为 null 表示某个群的横幅已移除,请用 im.calls.groupCallBanner(group_id) 重新读取各群的横幅 |
通话的详细用法见音视频通话。聊天室的消息、成员进出等在进入聊天室得到的对象上订阅,见聊天室。
用事件读取同步状态的值
以下值可以同步读取,变化时发出对应的事件,“订阅事件 + 读取”就能让界面保持最新:
| 读取 | 变化时的事件 |
|---|---|
im.auth.state、im.auth.currentUser | auth.stateChanged |
im.connection.state | connection.stateChanged |
im.connection.isLeader、im.connection.isAlertTab | tabs.roleChanged |
im.config | config.changed |
im.users.me() | me.changed |
im.users.myMute | me.muteChanged |
im.users.get(username) | users.changed |
im.conversations.getUnreadTotal() | conversations.unreadChanged |
im.conversations.getTyping(conversation_key) | typing.changed |
im.friends.get(username)、im.friends.listInfo | friends.changed |
im.friends.blacklist.has(username) | blacklist.changed |
im.friends.requests.unreadCount | friendRequests.changed |
im.messages.getReceipt(conversation_id, seq) | receipts.changed |
im.push.settings()、im.push.getMute(target) | push.changed |
im.presence.get(username) | presence.changed |
im.devices.online() | devices.changed |
im.calls.current、im.calls.incoming | call.incoming、call.changed |
im.calls.groupCallBanner(group_id) | groupCall.changed |
值没有变化时,读取返回同一个对象。登录、退出、切换用户、本地数据重新加载时,这些值也可能变化,对应的事件同样会发出。
const showUnread = () => render(im.conversations.getUnreadTotal());
im.on('conversations.unreadChanged', showUnread);
showUnread();错误对象
SDK 的所有错误都是 DRError:返回 Promise 的方法以它拒绝,同步方法以它抛出,message.sendFailed、sync.failed 等事件的载荷中也是它。
import { DRError } from '@deeprespond/im-web';
try {
await im.groups.leave('g_123');
} catch (err) {
if (!(err instanceof DRError)) throw err;
console.log(err.code, err.reason, err.details, err.status, err.requestId, err.retryAfter, err.source);
}| 字段 | 类型 | 说明 |
|---|---|---|
code | string | 错误码:服务端返回的错误码,或 SDK 自己的错误码(见下文) |
reason | string | undefined | 原因,即 details.reason |
details | Record<string, unknown> | undefined | 服务端返回的 details,原样;SDK 自己的错误中为补充信息 |
status | number | undefined | HTTP 状态码;长连接的错误和 SDK 自己的错误没有 |
requestId | string | undefined | 服务端的 request_id,联系技术支持时提供 |
retryAfter | number | undefined | 建议的等待时间,单位为秒 |
source | 'http' | 'ws' | 'local' | 错误的来源:HTTP 请求、长连接、SDK 本地 |
message | string | 调试用的说明,不要直接显示给用户,提示文字见显示错误提示 |
请按 code 和 reason 判断错误,不要按 status 或 message 判断。
与服务端错误码的关系
服务端返回的错误原样交给你,code、details 与服务端文档相同,source 为 http 或 ws。各错误码的含义和 details 的取值见服务端的错误码,客户端特有的错误码(如 user_muted、group_muted、not_friend、user_blocked)见其中的控制台和客户端接口的错误码。
SDK 在发出请求之前先在本地检查,确定会被服务端拒绝的直接拒绝,source 为 local:
- 功能没有开启、没有权限的(如应用没有开启群已读回执、普通成员 @所有人),沿用服务端的
permission_denied和原因,如read_ack_disabled、mention_all_denied、presence_disabled; - 超出服务端的固定频率限制的(如“全部已读”每分钟 2 次),沿用
rate_limited,带retryAfter; - 参数和内容不合规的(大小、格式、必填字段、不支持的字符等),一律为
local_validation,details.reason和details.field说明原因。local_validation总是表示请求没有发出。
这样你对“功能不可用、没有权限”和“太频繁”只需按服务端的错误码处理一次。
SDK 自己的错误码
code | 含义 | 建议 |
|---|---|---|
network_error | 网络不可用或请求没有送达 | 提示检查网络,稍后重试 |
timeout | 请求超时 | 稍后重试;写操作先确认结果,见离线与重试 |
aborted | 被你用 AbortSignal 取消 | 不提示 |
not_signed_in | 未登录时调用了需要登录的方法 | 先登录 |
not_connected | 必须走长连接的操作(如聊天室的部分操作)在未连接时调用 | 等连接就绪后再试 |
leader_unavailable | 同一浏览器的主标签页正在切换,调用没有完成 | 稍后重试,见多个标签页 |
local_validation | 本地检查不通过,请求没有发出。details.reason 为原因(如 required、too_large、control_characters、url_not_allowed),details.field 为字段 | 提示用户修改 |
storage_error | 读写本地存储出错 | 提示刷新页面 |
unsupported | 浏览器不满足运行环境要求,或服务端没有提供所需的服务(如没有开通文件服务时上传文件,details.reason 为 media_disabled) | 提示更换浏览器,或隐藏对应的功能 |
permission_required | 用户没有授权使用麦克风或摄像头,details.permissions 列出缺少的权限 | 引导用户在浏览器中授权 |
device_error | 麦克风或摄像头不存在或被占用,details.device 为 microphone 或 camera,details.reason 为 not_found 或 busy | 提示检查设备 |
invalid_state | 当前状态下不能调用,details.reason 说明原因:client_exists(这个页面已有同一个 AppKey 的客户端)、destroyed(客户端已释放)、disposed(列表已释放)、signed_in(不能清除当前用户的本地数据)、upgrade_required(SDK 版本过低,本地数据只读)等 | 一般是调用顺序的问题,检查代码 |
send_abandoned | SDK 放弃发送一条消息:details.reason 为 attachment_lost(附件已丢失,如页面关闭后超过 50 MB 的附件)或 expired(超过 48 小时没有发出) | 提示用户重新发送 |
local_validation 的原因
local_validation 的 details.reason 说明哪里不合规,details.field 为字段名(如 body.text、username):
details.reason | 含义 |
|---|---|
required | 必填的参数或字段为空 |
invalid_format | 格式不对,如 appKey、apiUrl、client_msg_id |
invalid_value | 取值不在允许的范围内 |
invalid_type | 参数的类型不对 |
out_of_range | 数值超出范围 |
too_long | 文字太长 |
too_large | 内容或文件太大,如消息超过运行配置的 max_message_body_bytes、附件超过 max_upload_bytes |
too_many_items | 一次传入的项目太多,如一次删除超过 100 条消息 |
control_characters | 文字中有不支持的控制字符(只有文本消息的正文允许换行、回车和制表符) |
invalid_body、unknown_type | 消息内容不符合消息类型的要求;不支持的消息类型 |
mentions_not_allowed、receipt_not_allowed | @ 提及、群已读回执只能用于群聊 |
url_not_allowed | 应用开启了只接受本服务的文件地址,这个地址不符合要求 |
invalid_purpose | 这种文件不能用于这里(如用视频作头像) |
invalid_ext、invalid_priority、invalid_attribute_value、attributes_too_large | 聊天室的扩展信息、消息优先级、属性不符合要求 |
too_many_invitees | 一次邀请通话的人太多 |
invalid_evidence、self_report | 举报的证据消息不符合要求;不能举报自己 |
not_found | 本地找不到要操作的会话或消息(如重发一条已不在发送队列中的消息) |
describeError 对其中常见的原因给出了专门的提示,其余显示“内容不符合要求”。
常见的服务端错误码
code | 含义 | 建议 |
|---|---|---|
invalid_argument | 参数不符合服务端的规则 | 提示用户修改;details.reason 说明原因 |
unauthenticated、session_revoked | 登录失效 | SDK 已自动处理(续期或结束会话),你只需处理 auth.stateChanged 中的 ended |
invalid_credentials | 用户名、密码或凭证错误 | 见登录失败 |
permission_denied | 没有权限,或功能没有开启,details.reason 说明原因 | 按原因提示,或隐藏对应的功能 |
not_found | 用户、群、会话、消息等不存在或已删除 | 提示“内容不存在或已删除” |
already_exists | 要创建的已存在 | 视为成功或提示 |
version_conflict | 数据已被修改 | 重新读取后再提交 |
limit_exceeded | 超出数量上限,details.reason 说明是哪一项 | 提示已达上限 |
payload_too_large | 内容或文件太大 | 提示用户缩短内容或换小一些的文件 |
rate_limited | 操作太频繁 | 按 retryAfter 稍后再试;SDK 已对可以安全重试的请求自动等待重试 |
too_many_attempts | 密码错误次数过多 | 按 retryAfter 提示稍后再试 |
message_rejected、content_rejected | 消息或资料没有通过内容安全检查 | 提示用户修改内容;reason 为 app_rejected 时,message 是你的服务端给出的提示,可以直接显示 |
user_muted、group_muted | 本人被全局禁言、在群中被禁言或群开启了全员禁言 | 提示禁言,details.expires_at 为到期时间 |
user_blocked | 被对方拉黑 | 提示“对方拒收了你的消息” |
not_friend | 应用要求只能给好友发消息,双方不是好友 | 提示先加好友 |
not_group_member | 本人已不在群中 | 提示并刷新界面 |
tenant_unavailable、app_unavailable | 服务暂停或只读 | 提示“服务暂时不可用”,见服务暂停 |
internal | 服务端故障 | 稍后重试,持续出现时带上 requestId 联系技术支持 |
显示错误提示
@deeprespond/im-web/locale/zh-CN 中的 describeError 按 code 和 reason 给出中文的提示文字,如“发送太频繁,请稍后再试”“群主已开启全员禁言”“你已被禁言,至 10 月 5 日 18:00”:
import { DRError } from '@deeprespond/im-web';
import { describeError } from '@deeprespond/im-web/locale/zh-CN';
im.on('message.sendFailed', ({ error }) => showToast(describeError(error)));
try {
await im.friends.requests.send({ username: 'lisi', message: '你好' });
} catch (err) {
if (err instanceof DRError) showToast(describeError(err));
}查找顺序为先找 code.reason(如 permission_denied.mention_all_denied),再找 code,都没有时显示通用的“服务器繁忙,请稍后重试”。例外:message_rejected、permission_denied 的原因为 app_rejected 时,显示服务端返回的 message(你的服务端给出的提示)。
要修改部分文案,或换成其他语言,传入第二个参数;文案表 zhCN.errors 的键就是上面说的 code 或 code.reason:
import { describeError, zhCN } from '@deeprespond/im-web/locale/zh-CN';
import type { DRError } from '@deeprespond/im-web';
const myLocale = {
errors: {
rate_limited: '手速太快啦,休息一下',
'permission_denied.mention_all_denied': '只有管理员可以 @所有人',
},
};
const tip = (err: DRError) => describeError(err, myLocale);
console.log(Object.keys(zhCN.errors)); // 内置的全部条目离线与重试
SDK 按错误的类型自动处理,你只需处理最终返回的错误:
| 类别 | 错误码 | SDK 的处理 |
|---|---|---|
| 登录失效 | unauthenticated、session_revoked | 令牌过期的自动续期并重发一次请求;会话被吊销的结束会话,见会话结束 |
| 服务暂停或只读 | tenant_unavailable、app_unavailable | 返回给你;确认是服务暂停的,进入服务暂停状态,恢复后自动继续 |
| 限流 | rate_limited、too_many_attempts | 等待时间不超过 60 秒的,等待后自动重试;更长的返回给你。发送好友申请、建群、邀请入群、申请入群、发起和邀请通话、举报、聊天室发消息,以及 too_many_attempts,不自动重试 |
| 暂时故障 | network_error、timeout、internal、HTTP 5xx | 可以安全重发的请求(查询、带幂等键的写操作、本身幂等的操作如标记已读)按退避自动重试,最多 3 次;其他写操作不自动重试,返回给你 |
| 需要用户处理 | 其余错误码 | 返回给你,按 code 和 details 提示 |
消息的发送另有发送队列:
im.messages.send()把消息写入发送队列后就返回,之后由 SDK 发送。断网时消息在队列中等待,网络恢复后自动发出;页面关闭后,下次打开时继续发送。- 发送遇到网络错误、超时、服务端故障时,按 1、2、4、8、16、30 秒的间隔重试,网络可用的时间累计 2 分钟仍未成功的标为失败;被限流的按服务端给出的时间等待后重试。
- 被服务端拒绝(如被禁言、被拉黑、内容未通过审核)的立即标为失败。
- 失败时消息状态变为
failed,并发出message.sendFailed。可以调用im.messages.resend(client_msg_id)重发,或im.messages.discard(client_msg_id)删除,见消息。 - 同一条消息重试时使用同一个
client_msg_id,服务端据此去重,不会发出两条。
没有自动重试的写操作(如建群、发送好友申请、修改群资料)遇到 network_error、timeout 时,请求可能已经执行。请先查询结果(如重新读取群列表),确认没有生效再让用户重试。
离线时:本地数据照常可读,界面可以继续显示;会话和消息的操作中只修改本地的(如草稿)立即生效,需要请求服务端的在自动重试后返回 network_error,发送消息进入发送队列。连接状态为 offline 或 waiting,网络恢复后 SDK 自动重连并同步离线期间的变化。
接口参考
im.on()
订阅事件。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
event | keyof DREvents | 是 | 事件名,见全部事件 |
listener | (payload: DREvents[E]) => void | 是 | 监听函数,参数为事件的载荷 |
返回值:() => void,取消订阅的函数,可以重复调用。客户端已释放时返回的函数什么也不做。
im.once()
订阅事件,只触发一次。参数和返回值与 im.on() 相同;在触发前调用返回的函数可以取消。
describeError()
从 @deeprespond/im-web/locale/zh-CN 导入。按错误码给出提示文字。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
err | DRError | { code: string; reason?: string; message?: string; details?: Record<string, unknown> } | 是 | 错误。也可以传入 auth.state 中 ended 的 code 和 reason |
locale | Partial<Locale> | 否 | 替换部分文案,其中的 errors 与内置的条目合并 |
返回值:string。
数据结构
DRError
继承自 Error,字段见错误对象。另有 name,固定为 'DRError'。
DREvents
事件名到载荷类型的映射,im.on() 和 im.once() 据此推断载荷的类型,各事件的载荷见全部事件。可以用它声明自己的事件处理函数:
import type { DREvents } from '@deeprespond/im-web';
function onReceived(p: DREvents['message.received']) {
console.log(p.message.conversation_id, p.alert);
}
im.on('message.received', onReceived);NotifyDecision
message.received 的 notify,说明这条消息是否应该提醒。
| 字段 | 类型 | 说明 |
|---|---|---|
notify | boolean | 是否应该提醒 |
title | string | 通知的标题,按推送设置的预览方式生成,可选 |
body | string | 通知的内容,可选 |
reason | string | 不提醒的原因,可选:dnd(勿扰中)、quiet_hours(勿扰时段)、muted(会话免打扰)、not_mentioned(免打扰为只在 @ 本人时提醒,而这条没有 @ 本人)、excluded(消息不计未读)、self(本人发的)、tip(群提示)、call(通话记录)、not_loaded(推送设置还没有加载) |
提醒的完整用法见提醒、推送设置与举报。
频道事件
channel.changed 的载荷为 {channel: ChannelHandle | null},表示当前本地频道句柄变化;只在持有频道媒体的页面处理,不假设其他标签页继承句柄。轨道、成员、退出、关闭和连接事件在 ChannelHandle 上订阅,完整表见频道事件。权限或心跳失败后不自动重建新会话。
