訊息
用 im.messages 顯示一個會話的訊息、傳送各種訊息,以及撤回、表情回應、置頂和已讀回執。會話列表見會話,上傳檔案見檔案。
快速開始
開啟一個會話的訊息列表,訂閱它的變化,再發一條訊息:
// 用户点开一个会话
const list = im.messages.open(conversationKey);
const off = list.subscribe(() => renderMessages(list.getSnapshot().items));
renderMessages(list.getSnapshot().items);
// 用户点击“发送”:消息立刻出现在列表末尾,发出后自动更新状态
await im.messages.send({ conversation_key: conversationKey }, { type: 'text', body: { text: '你好' } });
// 用户离开这个会话
off();
list.dispose();你只需要把 items 渲染出來。下面這些 SDK 會處理,不需要你寫程式碼:
| SDK 負責 | 說明 |
|---|---|
| 先顯示本地資料,再在後臺補齊 | 開啟過的會話重新整理頁面、斷網時也能立即顯示 |
| 新訊息、撤回、表情回應、編輯 | 自動更新到開啟著的列表中,不用自己合併 |
| 離線傳送和重試 | 沒網時訊息排隊,聯網後自動發出;失敗自動重試,不會重複 |
| 已讀與未讀數 | 使用者正在看的會話,新訊息不計入未讀 |
| 多個標籤頁 | 任一標籤頁都能傳送,各標籤頁的列表同時更新 |
conversationKey 來自會話列表中的 conversation_key,見會話。訊息的欄位(type、body、seq 等)與服務端相同,見訊息格式。
常見任務
| 要做什麼 | 用什麼 | 見 |
|---|---|---|
| 載入更早的訊息 | list.loadOlder() | 載入更早的訊息 |
| 從搜尋、置頂、@ 我 跳到某條訊息 | open(key, { around_seq })、list.jumpTo() | 跳轉到某條訊息 |
| 按型別顯示一條訊息 | type、body,以及 render 中的函式 | 顯示一條訊息 |
| 傳送圖片、語音、影片、檔案 | send() 直接傳 file | 傳送圖片 |
| 處理傳送失敗 | message.sendFailed 事件、resend()、discard() | 傳送失敗與重發 |
| @ 某人、引用回覆 | send() 的 mentions、reply_to | @ 提及、引用回覆 |
| 新訊息提醒(通知、提示音) | message.received 事件 | 接收新訊息 |
| 撤回、刪除 | recall()、deleteForMe() | 撤回、刪除訊息 |
| 表情回應、置頂 | react()、setPinned() | 表情回應、置頂訊息 |
| 顯示“已讀”“3 人未讀” | peer_read_seq、receipts() | 已讀回執 |
| 搜尋聊天記錄 | searchLocal() | 查詢訊息 |
顯示訊息列表
im.messages.open() 返回一個會持續更新的訊息列表(MessageList)。完整的渲染通常是這樣:
import type { MessageView } from '@deeprespond/im-web';
const list = im.messages.open(conversationKey);
function renderMessages(): void {
const { items, gaps, hasOlder, loading, pinned, error } = list.getSnapshot();
render({
messages: items.map((m: MessageView) => ({
key: m.seq !== null ? `s${m.seq}` : `p${m.client_msg_id}`,
mine: m.sender === im.auth.currentUser?.username,
status: m._local.status, // sent / queued / uploading / sending / failed
message: m,
})),
gaps,
showLoadOlder: hasOlder,
spinner: loading !== 'none',
pinned,
error,
});
}
const unsubscribe = list.subscribe(renderMessages);
renderMessages();
// 离开这个会话时
unsubscribe();
list.dispose();快照(getSnapshot() 的返回值)中的欄位:
| 欄位 | 用來做什麼 |
|---|---|
items | 訊息,按序號從舊到新;還沒發出的訊息排在最後 |
hasOlder | 上面還有更早的訊息,顯示“載入更多” |
loading | 'initial'(正在開啟)、'older'、'newer' 或 'none',用來顯示載入動畫 |
error | 載入出錯時的錯誤 |
pinned | 這個會話的置頂訊息,見置頂訊息 |
gaps | 中間暫時沒有載入的訊息,見空洞 |
訊息的 key:已發出的訊息用 seq(會話內的序號);還沒發出的訊息 seq 為 null,用 client_msg_id。上面的例子就是這樣做的。
一定要 dispose():不釋放的話,SDK 會繼續為這個列表補齊訊息,這個會話也會一直被當作“使用者正在看”,新訊息不計入未讀。
按引用比較:沒有變化時 getSnapshot() 返回同一個物件;有變化時,沒變的訊息仍是原來的物件,可以直接用於 React 的 memo 等最佳化。快照是隻讀的。
同一個會話可以同時開啟多個列表(如主視窗和彈出的小視窗),它們共用本地資料。
先顯示本地資料
開啟時,SDK 先顯示本地儲存的最新 50 條,再在後臺向服務端補齊離線期間的新訊息,補齊後快照更新:
| 情況 | 表現 |
|---|---|
| 開啟過的會話 | 重新整理頁面、離線時也能立即顯示 |
| 新裝置上第一次開啟 | 本地沒有訊息,loading 為 'initial',取回最新一頁後顯示 |
| 列表開啟期間有新訊息 | 自動補齊,不需要使用者操作 |
| 超過應用訊息保留期的訊息 | 不再顯示,即使本地還有 |
告訴 SDK 使用者是否在看
使用者“正在看”一個會話時,新訊息不計入未讀;建立客戶端時開啟了 autoMarkRead 的,還會自動標記已讀。
SDK 預設認為開啟的列表“停在最新處、正在顯示”。以下兩種情況請告訴它:
- 使用者向上翻看歷史,不在最新處了;
- 列表被隱藏,如 Vue
<KeepAlive>快取了頁面、標籤頁切到了別的面板。
scroller.addEventListener('scroll', () => {
const atBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < 40;
list.setViewing({ atBottom });
if (atBottom) list.markRead();
});
// 面板被切走、再切回时
list.setViewing({ visible: false });
list.setViewing({ visible: true });list.markRead() 把已讀位置推進到最新一條,與 im.conversations.markRead() 相同,連續呼叫時 SDK 合併成一次請求。瀏覽器標籤頁不可見時,SDK 自動按“沒在看”處理。
載入更早的訊息
使用者滾到頂部、hasOlder 為 true 時呼叫 loadOlder(),每次 50 條:
async function onReachTop(): Promise<void> {
const { hasOlder, loading } = list.getSnapshot();
if (hasOlder && loading === 'none') await list.loadOlder();
}本地有的直接讀取,沒有的向服務端獲取。失敗時快照帶 error,提示使用者後可以再次呼叫。
跳轉到某條訊息
從搜尋結果、置頂訊息、引用或“有人 @ 我”進入時,開啟列表並定位到那條訊息:
// 打开会话并定位到第 1024 条(取它前后各约 25 条)
const around = im.messages.open(conversationKey, { around_seq: 1024 });
// 已打开的列表中跳转
await list.jumpTo(1024);
// 定位后向下翻到最新
if (list.getSnapshot().hasNewer) await list.loadNewer();定位後列表不在最新處:hasNewer 為 true,還沒發出的訊息暫不顯示。使用者向下滾動時呼叫 loadNewer(),翻到最新後恢復正常。要直接回到最新處,釋放這個列表,再不帶 around_seq 重新開啟。
“有人 @ 我”的訊息序號在會話狀態的 mention_seq 中,見會話。
中間缺失的訊息(空洞)
gaps 列出列表中間暫時沒有載入的訊息,每項是一段序號範圍(from_seq 到 to_seq,含兩端)。在對應位置顯示一個提示:
kind | 什麼時候出現 | 顯示什麼 |
|---|---|---|
skipped | 長時間離線後會話積累了很多新訊息,SDK 只補齊了最新的一部分 | “檢視更早的訊息”,點選時呼叫 loadGap(gap)。不要當作“沒有訊息” |
rejoin | 退群后又重新入群,兩次入群之間的訊息本人看不到 | “以下為你重新加入群聊之前的訊息”這類分隔,不能載入 |
import type { Gap } from '@deeprespond/im-web';
function onGapClick(gap: Gap): void {
if (gap.kind === 'skipped') void list.loadGap(gap);
}遇到 skipped 空洞時 hasOlder 為 false,由使用者點選空洞繼續向上載入。
顯示一條訊息
列表中的每一條是 MessageView:服務端的訊息物件,加上 SDK 的本地狀態 _local。按 type 顯示:
import type { MessageView } from '@deeprespond/im-web';
import { displayName, renderCallRecord, renderRecall, renderTip } from '@deeprespond/im-web/render';
function describe(m: MessageView): string {
const me = im.auth.currentUser?.username ?? '';
const group_id = m.conversation_type === 'group' ? m.conversation_id : undefined;
const nameOf = (u: string) => displayName(im, u, { group_id });
if (m.recalled) return renderRecall(m, { self: me, nameOf });
if (m.erased) return '[消息已删除]';
if (m._local.unsupported) return '[当前版本不支持此消息]';
const body = (m.body ?? {}) as Record<string, unknown>;
switch (m.type) {
case 'text':
return String(body.text);
case 'tip':
return renderTip(m, { nameOf, self: me });
case 'call':
return renderCallRecord(m, { self: me });
case 'custom':
// 涉及资金、订单的卡片只信任不是由用户在客户端发出的
return m._local.trusted ? `[卡片] ${String(body.title ?? '')}` : '[消息]';
default:
return `[${m.type}]`;
}
}各部分怎麼顯示:
| 內容 | 怎麼判斷 | 怎麼顯示 |
|---|---|---|
| 傳送者 | sender 是使用者名稱 | 用 displayName() 顯示好友備註、群暱稱或暱稱 |
| 系統訊息 | sender_type 為 'system',sender 為 null | 群提示、以系統身份發的群訊息,居中顯示 |
| 已刪除的使用者 | sender_type 為 'user',sender 為 null | “已刪除的使用者” |
| 不認識的型別 | _local.unsupported 為 true | “當前版本不支援此訊息”,不要丟棄 |
| 可信的卡片 | _local.trusted 為 true | 由你的服務端、控制檯或系統寫入。涉及資金、訂單的 custom 卡片只信任這種 |
| 已撤回、已擦除 | recalled 不為 null,或 erased 為 true | 內容已清空,顯示提示;撤回用 renderRecall() |
| 已編輯 | edited 不為 null | 可以顯示“已編輯”,見編輯 |
| 引用 | reply_to 為 { seq, sender } | 按序號在列表中找原訊息;找不到用 im.messages.lookup() 取回;原訊息已撤回的顯示“原訊息已撤回” |
| 圖片、語音、影片、檔案 | body.url 是檔案地址 | 不能直接放進 <img src>,要先換取下載地址,見檔案 |
| 時間 | created_at,ISO 8601 字串 | 自己用 Intl.DateTimeFormat 或日期庫格式化;還沒發出的為 null,可顯示“剛剛” |
傳送訊息
im.messages.send(to, content, opts?) 傳送一條訊息。to 指定發給誰:
to | 說明 |
|---|---|
{ conversation_key } | 發到一個已開啟的會話,單聊、群聊和草稿會話都可以 |
{ to_user: 'lisi' } | 發給一個使用者,還沒有會話也可以 |
{ to_group: groupId } | 發到一個群 |
傳送文字
const local = await im.messages.send({ conversation_key: conversationKey }, { type: 'text', body: { text: '你好' } });
console.log(local._local.status); // 'queued'send() 不等訊息發出就返回,訊息的狀態會依次變化,列表隨之更新:
_local.status | 含義 |
|---|---|
queued | 在傳送佇列中等待 |
uploading | 正在上傳附件(圖片、檔案等) |
sending | 正在傳送 |
sent | 已發出,列表中這條換成服務端的正式訊息,帶 seq、message_id、created_at |
failed | 傳送失敗,見傳送失敗與重發 |
需要等發出後再繼續的(如傳送後跳轉頁面),設定 waitUntil: 'sent',發出後返回正式訊息,最終失敗時丟擲錯誤:
const sent = await im.messages.send({ to_group: groupId }, { type: 'text', body: { text: '会议纪要已上传' } }, { waitUntil: 'sent' });
console.log(sent.seq, sent.message_id);呼叫時 SDK 先在本地檢查型別、必填欄位、控制字元、大小和 @ 的規則,不透過的直接丟擲錯誤,訊息不會進入傳送佇列。文字不能為空,可以包含換行。
傳送佇列
| 情況 | 行為 |
|---|---|
| 沒有網路 | 訊息排隊,狀態為 queued,聯網後自動發出;頁面重新整理、瀏覽器重啟後繼續 |
| 順序 | 同一會話按呼叫順序逐條發出;不同會話並行 |
| 連續發很多條 | SDK 自動限速(每個會話每秒 4 條,合計每秒 8 條),不會觸發服務端限流 |
| 多個標籤頁 | 任一標籤頁都能呼叫 send(),由其中一個統一傳送 |
| 48 小時還沒發出 | 不再自動傳送,標為失敗(send_abandoned,reason 為 expired),由使用者決定是否重發 |
關閉頁面前,im.hasPendingWork() 為 true 表示還有訊息沒發出或上傳沒完成,可以提醒使用者:
window.addEventListener('beforeunload', (e) => {
if (im.hasPendingWork()) e.preventDefault();
});不提醒也不會丟:下次開啟頁面時繼續傳送。
傳送失敗與重發
網路錯誤、超時和伺服器暫時故障,SDK 自動重試(間隔 1、2、4、8、16、30 秒),不會產生重複訊息;網路正常但連續失敗滿 2 分鐘的標為失敗。被服務端拒絕的(如被禁言、對方拒收)立即標為失敗,不重試。
失敗的訊息留在列表中,_local.status 為 'failed',_local.error 是原因,同時發出 message.sendFailed 事件。讓使用者選擇重發或刪除:
import { describeError } from '@deeprespond/im-web/locale/zh-CN';
im.on('message.sendFailed', ({ error }) => {
showToast(describeError(error)); // 如“你已被禁言”“对方拒收了你的消息”
});
// 用户点击“重发”
await im.messages.resend(failed.client_msg_id!);
// 用户点击“删除”(只删除本地的这条,没完成的上传一并取消)
await im.messages.discard(failed.client_msg_id!);resend()沿用原來的client_msg_id,服務端據此去重;上傳失敗的附件從頭重新上傳。_local.status_unknown為true的失敗訊息可能其實已經發出(重試期間應用改了規則),可以顯示“傳送狀態未知”。下次同步後 SDK 會核對,確實發出的自動換成正式訊息。
常見的失敗原因:
code | 原因 |
|---|---|
user_muted | 本人被禁言 |
group_muted | 群裡被禁言:reason 為 member 是本人被禁言,all 是全員禁言;details.muted_until 為到期時間 |
not_group_member | 已不在群中 |
user_blocked | 對方拒收 |
not_friend | 應用要求是好友才能聊天 |
message_rejected | 內容沒透過稽核;reason 為 app_rejected 時,message 是你的服務端給出的提示 |
not_found | 對方不存在或已刪除 |
payload_too_large | 超過訊息大小上限 |
傳送圖片
把使用者選的檔案直接交給 send(),SDK 處理、上傳後再傳送:
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
if (!file) return;
try {
await im.messages.send({ conversation_key: conversationKey }, { type: 'image', file });
} catch (err) {
showToast(err); // 调用时的检查不通过,如服务端没有开通文件服务
}
});- 上傳中:訊息立即出現在列表中,
_local.status為'uploading',_local.progress為 0 到 1 的進度。可以用URL.createObjectURL(file)先顯示本地預覽。 - 壓縮:長邊縮小到 2048 畫素以內並重新編碼,去掉拍攝地點等後設資料;GIF 不處理。要發原圖,設定
original: true(只去掉後設資料)。 - 自動改為檔案:超過 20 MB 或 5000 萬畫素、瀏覽器無法解碼(如 HEIC)的圖片,自動改為檔案訊息傳送,
type變為file。語音、影片格式不被服務端接受時也一樣。 body中的尺寸:url、thumbnail_url、width、height、size、format由 SDK 填寫,你給的會被忽略;可以加你自己的欄位。- 重新整理頁面:上傳到一半時頁面被重新整理,重新開啟後繼續上傳(50 MB 以內);更大的標為失敗(
send_abandoned,reason為attachment_lost),請使用者重新選擇。
只上傳不傳送、下載和顯示圖片,見檔案。
傳送語音
用 im.files.recordVoice() 錄音,結束後得到錄音和時長:
const recorder = await im.files.recordVoice(); // 用户没有允许使用麦克风时以 permission_required 拒绝
await recorder.start();
// 用户松开“按住说话”
const { blob, duration_seconds } = await recorder.stop();
await im.messages.send({ conversation_key: conversationKey }, { type: 'voice', file: blob, body: { duration_seconds } });必須給出時長 body.duration_seconds(秒,向上取整,至少 1)。語音檔案不超過 5 MB。瀏覽器不支援錄音時丟擲 unsupported;使用者取消時呼叫 recorder.cancel()。
傳送影片
await im.messages.send({ conversation_key: conversationKey }, { type: 'video', file });SDK 讀取時長和寬高,擷取第一幀作為封面一起上傳。服務端不接受的影片格式自動改為檔案訊息。
傳送檔案
await im.messages.send({ conversation_key: conversationKey }, { type: 'file', file, body: { name: '季度报告.pdf' } });body.name 是顯示的檔名,省略時用 File 的檔名。檔案不能超過應用的上傳上限(執行配置的 max_upload_bytes),超過時丟擲 local_validation(reason 為 too_large,details.max_bytes 為上限)。
傳送已有地址的附件
檔案已經上傳過(用 im.files.upload(),或在你自己的檔案伺服器上)時,直接給出 body,不再上傳:
await im.messages.send({ conversation_key: conversationKey }, {
type: 'image',
body: { url: 'https://im.example.com/media/v1/f/99606017553203200/5nOzK6E40hgYIKl80dYVLQ', width: 1080, height: 720 },
});url 必填;寬高、大小、時長必須是非負整數。應用開啟了“只允許本服務的檔案地址”(media_url_only)時,外部地址會被拒絕,見訊息格式。
傳送位置
await im.messages.send({ conversation_key: conversationKey }, {
type: 'location',
body: { latitude: 39.9087, longitude: 116.3975, name: '天安门', address: '北京市东城区' },
});緯度在 -90 到 90 之間,經度在 -180 到 180 之間。
傳送自定義訊息
custom 訊息的 body 是任意 JSON 物件,由你定義,如訂單卡片、名片、業務通知:
await im.messages.send({ conversation_key: conversationKey }, {
type: 'custom',
body: { card: 'order', order_id: '8812', title: '你的订单已发货' },
});字串中不能有控制字元(包括換行)。使用者在客戶端也能發出看起來像“轉賬”“訂單”的卡片,所以接收方只應信任 _local.trusted 為 true 的這類訊息,它們由你的服務端發出。
@ 提及
群訊息可以 @ 成員或全體成員。正文裡的“@李四”由你自己拼,SDK 只提交 mentions:
await im.messages.send({ to_group: groupId }, { type: 'text', body: { text: '@李四 @王五 请确认' } }, {
mentions: { usernames: ['lisi', 'wangwu'] },
});
// @ 全体成员(只有群主和管理员可以)
await im.messages.send({ to_group: groupId }, { type: 'text', body: { text: '@所有人 明天放假' } }, {
mentions: { all: true },
});| 限制 | 違反時 |
|---|---|
| 只能用於群聊 | local_validation(reason 為 mentions_not_allowed) |
| 一條最多 @ 20 人;不是群成員的會被忽略 | — |
| 只有群主和管理員能 @ 全體成員 | permission_denied(reason 為 mention_all_denied) |
被 @ 的人在會話列表中看到“有人 @ 我”,見會話。
引用回覆
await im.messages.send({ conversation_key: conversationKey }, { type: 'text', body: { text: '同意' } }, {
reply_to: { seq: quoted.seq! },
});只需要給出被引用訊息的序號。它必須在同一會話中且沒有被撤回,否則服務端返回 invalid_argument(reason 為 invalid_reply)。
其他選項
| 選項 | 說明 |
|---|---|
ext | 你的擴充套件欄位(JSON 物件),原樣儲存和返回,如業務 ID、轉發來源 |
need_receipt | 要求群已讀回執,只用於群聊,見群聊的已讀回執 |
exclude_from_unread | 不計入接收者的未讀數,適合不需要提醒的業務訊息 |
push | { disabled: true } 時不給接收者發離線推送 |
waitUntil | 'queued'(預設)進入傳送佇列就返回;'sent' 發出後返回 |
一條訊息的型別、body、ext、mentions 和 reply_to 合計不能超過應用的大小上限(執行配置的 max_message_body_bytes,預設 5 KB)。
給還沒有會話的人發訊息
用 to_user 直接發,或者先用 im.conversations.openSingle() 開啟單聊:
const view = await im.conversations.openSingle('lisi');
const list = im.messages.open(view.conversation_key); // 还没有会话时为 draft:lisi
await im.messages.send({ conversation_key: view.conversation_key }, { type: 'text', body: { text: '你好,我是张三' } });還沒有會話時,訊息先放在草稿會話 draft:lisi 中,conversation_id 為 null。第一條訊息發出後服務端建立會話,SDK 自動把草稿換成正式會話:
- 已開啟的列表自動切換,
conversation_key變為新的會話 ID; messages.changed事件帶上renamed_from(原來的草稿鍵),如果你用conversation_key做了路由或快取,據此更新。
對方不存在、已刪除或就是本人時,訊息傳送失敗(not_found 或 invalid_argument)。
轉發
forward() 把一條訊息的型別和內容作為新訊息發給別人,附件沿用原來的地址,不重新上傳:
await im.messages.forward(message, { to_user: 'wangwu' }, { ext: { forwarded_from: message.message_id } });群提示、通話記錄、已撤回和已擦除的訊息不能轉發(local_validation)。附件按上傳時間過期,過期後轉發的訊息同樣下載不了。合併轉發請用 custom 訊息自己定義。
只推線上的訊息
“對方正在錄音”、白板筆跡這類實時信令不需要儲存,用 sendOnline():只推給接收者此刻線上的裝置,不儲存、不計入未讀,離線的裝置收不到,之後也拉取不到。
await im.messages.sendOnline({ to_group: groupId }, { body: { signal: 'whiteboard', stroke: [1, 2, 3, 4] } });
im.on('message.online', ({ message }) => {
console.log(message.sender, message.body);
});- 只能是
custom型別。不進傳送佇列,失敗不重試,沒有網路時直接丟擲錯誤。 - 接收方的
message.online事件已去重,這些訊息不出現在訊息列表中。 - “正在輸入”請用
im.conversations.typing(),見會話。
接收新訊息
開啟著的訊息列表、會話列表和未讀數都會自動更新,一般不需要處理新訊息事件。需要提醒使用者(瀏覽器通知、提示音)時,監聽 message.received:
im.on('message.received', ({ message, alert }) => {
if (alert) console.log('新消息', message.conversation_id, message.seq);
});- 只對實時收到的、別人發來的訊息觸發。本人在其他裝置發的、離線期間的訊息(由同步補齊)不觸發。
- 每個標籤頁都會收到這個事件,只在
alert為true的那個標籤頁提醒,避免重複。載荷中的notify是 SDK 按推送設定和免打擾給出的建議,用法見提醒、推送設定與舉報。
要監聽某個會話的變化(新訊息、撤回、表情回應、傳送狀態),用 messages.changed 事件:
im.on('messages.changed', ({ conversation_key, upserted, removed, pending, renamed_from }) => {
// upserted:新增或变化的消息序号;removed:删除的序号;pending:发送队列中变化的 client_msg_id
console.log(conversation_key, upserted, removed, pending, renamed_from);
});本人的訊息發出時,同一個事件裡 pending 帶它的 client_msg_id,upserted 帶它的新序號,可以據此保持滾動位置不跳。
撤回
if (im.messages.canRecall(message)) {
await im.messages.recall(message.conversation_id, message.seq!);
}用 canRecall() 決定是否顯示“撤回”選單。規則:
| 誰 | 能撤回什麼 |
|---|---|
| 單聊 | 只能撤回本人的訊息,在撤回時限內(執行配置的 recall_window_seconds,預設 120 秒) |
| 群裡的普通成員 | 本人的訊息,在時限內 |
| 群管理員 | 本人的訊息(時限內),以及普通成員的訊息 |
| 群主 | 任何人的訊息,不受時限 |
- 群提示、通話記錄、系統訊息不能撤回;還沒發出的訊息用
discard()刪除。 - 已離開的群中不能撤回(
permission_denied,read_only_membership)。canRecall()不檢查這一點,請同時判斷會話的state.membership是否為member。 - 超過時限返回
permission_denied(reason為recall_window_expired),沒有許可權為recall_denied。
撤回後雙方都看到已撤回:recalled 為 { by, role, at },內容被清空。用 renderRecall() 顯示“你撤回了一條訊息”“張三撤回了一條訊息”“張三撤回了李四的一條訊息”;由你的服務端或內容安全撤回的(role 為 server 或 platform)顯示“一條訊息已被撤回”。
編輯
Web SDK 不能編輯訊息。你的服務端可以編輯 text 和 custom 訊息(見撤回、編輯與置頂),編輯後客戶端的列表自動更新,edited 為 { at, count },可以顯示“已編輯”。
刪除訊息
deleteForMe() 只從本人這裡刪除,對方和其他群成員不受影響,本人的其他裝置同步刪除:
await im.messages.deleteForMe(conversationId, [101, 102, 105]);一次最多 100 條。沒有雙向刪除,要讓對方也看不到請撤回。還沒發出的訊息用 discard() 刪除。
表情回應
const id = message.conversation_id;
const seq = message.seq!;
// 添加、取消
await im.messages.react(id, seq, 'thumbs_up');
await im.messages.unreact(id, seq, 'thumbs_up');
// 显示:每种表情的人数、前 3 个回应的人,以及本人是否回应过
for (const r of message.reactions ?? []) {
const mine = message._local.reacted?.[r.key] ?? r.reacted ?? false;
console.log(r.key, r.count, r.users, mine);
}
// 回应这种表情的全部人(翻页)
const page = await im.messages.reactionUsers(id, seq, 'thumbs_up', { limit: 50 });
console.log(page.items, page.next_cursor);- 表情的
key由你定義(如thumbs_up、heart),各端保持一致。 - 需要應用開啟表情回應(執行配置的
message_reaction_enabled),否則返回permission_denied(reaction_disabled)。 - 每條訊息最多
max_reactions_per_message種,滿了返回limit_exceeded(reaction_limit)。 - 群提示、已撤回的訊息不能回應;已離開的群中不能回應,也不能檢視名單(
read_only_membership)。 - 返回值的
changed為false表示本來就是這個狀態。別人的回應會實時更新到列表中。
置頂訊息
await im.messages.setPinned(message.conversation_id, message.seq!, true);
// 消息列表的快照中有这个会话的置顶消息,按置顶时间倒序
for (const item of list.getSnapshot().pinned) {
console.log(item.seq, item.pinned.by, item.message?.body ?? '加载中');
}- 單聊雙方都能置頂;群聊只有群主和管理員能置頂(否則
permission_denied,pin_denied)。 - 每個會話最多
max_pinned_messages_per_conversation條(滿了返回limit_exceeded,pinned_message_limit)。 - 正文還沒取回的置頂訊息
message為null,取回後快照更新。點選置頂訊息時用jumpTo(item.seq)跳過去。
已讀回執
單聊
單聊不需要單獨查詢:會話狀態中的 peer_read_seq 是對方已讀到的序號,本人發的、seq 不大於它的訊息顯示“已讀”。對方讀了之後實時更新。
const view = await im.conversations.get(list.conversation_key);
const peerRead = view?.state?.peer_read_seq ?? 0;
const me = im.auth.currentUser?.username;
const enabled = im.config?.single_read_ack_enabled === true;
for (const m of list.getSnapshot().items) {
if (enabled && m.sender === me && m.seq !== null) {
console.log(m.seq, m.seq <= peerRead ? '已读' : '未读');
}
}應用沒有開啟單聊已讀回執(執行配置的 single_read_ack_enabled)時不要顯示已讀狀態。請按這個配置判斷,不要按 peer_read_seq 是否為 null 判斷。
群聊
群訊息的已讀回執要在傳送時設定 need_receipt: true,而且只有傳送者本人(仍在群裡時)能檢視。應用需要開啟群已讀回執(group_read_ack_enabled),否則傳送時返回 permission_denied(read_ack_disabled)。
await im.messages.send({ to_group: groupId }, { type: 'text', body: { text: '请大家确认' } }, { need_receipt: true });
// 查询已读人数:可以逐条调用,SDK 合并请求
const counts = await im.messages.receipts(groupId, seqs);
for (const c of counts) {
if ('read_count' in c) console.log(c.seq, `${c.read_count} 人已读,${c.unread_count} 人未读`);
else console.log(c.seq, '不能查看', c.code);
}
// 已读人数变化时(成员读到了你的消息)
im.on('receipts.changed', ({ conversation_id, seqs: changed }) => {
for (const seq of changed) console.log(seq, im.messages.getReceipt(conversation_id, seq));
});
// 已读、未读的人
const readers = await im.messages.receiptUsers(groupId, seqs[0]!, { status: 'read', limit: 50 });
console.log(readers.items, readers.next_cursor);receipts():適合在訊息滾進視野時逐條呼叫,SDK 會合並、去重(10 秒內查過的不再查)、分批請求。不能檢視的那條(不是本人發的、沒要求回執)在結果中為{ seq, code },不影響其他條。本人已離開群時返回permission_denied(receipt_denied)。getReceipt():同步讀取記憶體中最近的人數,沒查過時為undefined。- 實時更新:500 人以內的群,成員讀到訊息時人數實時推送,SDK 發出
receipts.changed;更大的群只能主動查詢。 - 頻率:查詢回執、回執名單和表情回應名單合計每人每分鐘 60 次。
查詢訊息
按序號取回:引用、置頂、通知中提到的訊息不在列表中時,用 lookup() 從服務端取回(同時儲存到本地):
const { items, missing } = await im.messages.lookup(conversationId, [3, 57]);
// missing:不存在、已过期或本人看不到的序号搜尋聊天記錄:服務端不提供訊息搜尋。searchLocal() 在本瀏覽器儲存過的文字訊息中按關鍵詞查詢(不區分大小寫),按時間倒序:
import type { MessageView } from '@deeprespond/im-web';
const results = await im.messages.searchLocal({ keyword: '季度报告', limit: 20 });
// 用户点击一条结果时,打开会话并定位到这条消息
function onResultClick(m: MessageView) {
return im.messages.open(m.conversation_id, { around_seq: m.seq! });
}只能找到本地儲存過的訊息(開啟過、同步過的會話)。傳 conversation_id 可以只在一個會話中查詢。
顯示輔助
@deeprespond/im-web/render 提供拼出顯示文字的函式,@deeprespond/im-web/locale/zh-CN 提供中文文案。都是可選的,你也可以自己實現。
import { conversationTitle, displayName, renderCallRecord, renderRecall, renderTip, summarize } from '@deeprespond/im-web/render';| 函式 | 用途 | 示例輸出 |
|---|---|---|
displayName(im, username, opts?) | 使用者的顯示名 | 李四 |
conversationTitle(im, view) | 會話標題:單聊為對方的顯示名,群聊為群名 | 產品討論組 |
summarize(message, opts) | 會話列表中最後一條訊息的摘要 | [圖片]、[檔案] 報告.pdf |
renderTip(message, opts) | 群提示的文字 | 張三邀請李四、王五加入了群聊 |
renderCallRecord(message, opts) | 通話記錄的文字 | 視訊通話 通話時長 03:12、未接來電 |
renderRecall(message, opts) | 撤回提示 | 你撤回了一條訊息 |
顯示名
displayName(im, username, { group_id }) 按順序取第一個非空的:你給他設的好友備註、他在這個群的群暱稱(給出 group_id 且本地有群成員資料時)、他的暱稱、使用者名稱。不在群裡顯示時省略 group_id。
import { displayName } from '@deeprespond/im-web/render';
const inChat = displayName(im, 'lisi');
const inGroup = displayName(im, 'lisi', { group_id: '99582580415791104' });- 同步返回:本地沒有這個使用者的資料時先返回使用者名稱,同時在後臺獲取,獲取後發出
users.changed,重新渲染即可顯示暱稱。備註、群暱稱、群名變化時同樣會發出事件(friends.changed、groupMembers.changed、groups.changed)。 nameOf引數:把使用者名稱轉為顯示名,通常寫成(u) => displayName(im, u, { group_id })。self引數:本人的使用者名稱。群提示、撤回提示中本人顯示為“你”;通話記錄按本人是不是發起方顯示“已取消”或“對方已取消”。- 兜底文字:已刪除的使用者顯示“已刪除的使用者”;不認識的群提示顯示“[群提示]”,不認識的訊息型別摘要為“[訊息]”。
替換文案
文案來自 zhCN 表。每個函式都接受 locale 引數,可以替換部分條目,或整個換成其他語言:
import { summarize } from '@deeprespond/im-web/render';
import { zhCN, type Locale } from '@deeprespond/im-web/locale/zh-CN';
const en: Partial<Locale> = {
you: 'You',
deletedUser: 'Deleted user',
summary: { ...zhCN.summary, image: '[Photo]', voice: '[Voice]', video: '[Video]', file: '[File] {name}' },
};
const text = summarize({ type: 'image', body: {}, sender: 'lisi', sender_type: 'user', recalled: null }, { nameOf: (u) => u, locale: en });
// '[Photo]'替換某一組(如 summary、tip、recall、call)時,先展開 zhCN 中的這一組再改,沒給出的條目沿用中文。{name} 等佔位符原樣保留。
describeError(err) 按錯誤碼給出中文提示,見事件與錯誤。
介面參考
im.messages.open()
開啟一個會話的訊息列表。每次呼叫返回一個新的列表,不再使用時呼叫它的 dispose()。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_key | string | 是 | 會話鍵:會話 ID、群 ID,或草稿會話的 draft:{username} |
opts.around_seq | number | 否 | 定位到這個序號附近 |
返回值:MessageList,見 MessageList。
可能的錯誤:同步丟擲 not_signed_in、local_validation(reason 為 required)。
im.messages.send()
傳送一條訊息:檢查後寫入傳送佇列,帶檔案的先上傳。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
to | SendTarget | 是 | 發給誰,見 SendTarget |
content | OutgoingContent | 是 | 訊息的型別和內容,見 OutgoingContent |
opts | SendOptions | 否 | 見 SendOptions |
返回值:Promise<MessageView>。預設為寫入傳送佇列後的本地訊息(seq 為 null);waitUntil: 'sent' 時為發出後的正式訊息。
可能的錯誤:
- 呼叫時:
local_validation(reason為required、control_characters、invalid_value、invalid_type、too_large、mentions_not_allowed、receipt_not_allowed、too_many_items、unknown_type、not_found,details.field指出欄位)、permission_denied(mention_all_denied、read_ack_disabled)、unsupported(media_disabled,服務端沒有開通檔案服務)、invalid_state(peer_deleted,單聊的對方已刪除)、not_signed_in; waitUntil: 'sent'時還有傳送失敗的錯誤,與message.sendFailed的error相同。
im.messages.resend()
重發一條失敗的訊息。不是失敗狀態的什麼也不做。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
client_msg_id | string | 是 | 失敗訊息的 client_msg_id |
返回值:Promise<void>,重新進入傳送佇列後兌現。
可能的錯誤:local_validation(reason 為 not_found,傳送佇列中沒有這條)。
im.messages.discard()
刪除傳送佇列中的一條訊息(失敗的或還在等待的),取消沒完成的上傳。隻影響本地。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
client_msg_id | string | 是 | 訊息的 client_msg_id |
返回值:Promise<void>。
im.messages.sendOnline()
傳送只推線上的 custom 訊息。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
to | SendTarget | 是 | 發給誰 |
content.body | Record<string, unknown> | 是 | 訊息體 |
content.ext | Record<string, unknown> | 否 | 擴充套件欄位 |
返回值:Promise<OnlineResult>,見 OnlineResult。
可能的錯誤:local_validation、network_error,以及服務端拒絕傳送的錯誤(與普通訊息相同)。
im.messages.forward()
把一條訊息的型別和內容作為新訊息傳送。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | MessageView | 是 | 要轉發的訊息 |
to | SendTarget | 是 | 發給誰 |
opts | SendOptions | 否 | 同 send() |
返回值:Promise<MessageView>,同 send()。
可能的錯誤:local_validation(reason 為 unknown_type,群提示和通話記錄;invalid_body,已撤回、已擦除;url_not_allowed,應用開啟 media_url_only 而附件地址不允許),以及 send() 的錯誤。
im.messages.recall()
撤回一條訊息。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 會話 ID |
seq | number | 是 | 訊息的序號 |
返回值:Promise<{ changed: boolean }>,changed 為 false 表示已經撤回過。
可能的錯誤:permission_denied(recall_window_expired、recall_denied、read_only_membership)、not_found。
im.messages.canRecall()
同步判斷本人能否撤回這條訊息,用於決定是否顯示撤回入口。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
message | MessageView | 是 | 訊息 |
返回值:boolean。
im.messages.deleteForMe()
刪除本人的訊息(僅自己)。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 會話 ID |
seqs | number[] | 是 | 訊息的序號,最多 100 個;空陣列時什麼也不做 |
返回值:Promise<void>。
可能的錯誤:local_validation(reason 為 too_many_items)、not_found。
im.messages.react()
新增表情回應。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 會話 ID |
seq | number | 是 | 訊息的序號 |
key | string | 是 | 表情的標識 |
返回值:Promise<{ changed: boolean }>。
可能的錯誤:permission_denied(reaction_disabled、message_recalled、read_only_membership)、limit_exceeded(reaction_limit)、invalid_argument(invalid_reaction_key)、user_blocked、not_friend、local_validation(reason 為 required,沒有給出 key)。
im.messages.unreact()
取消本人的表情回應。引數和返回值同 react()。
im.messages.reactionUsers()
查詢回應某種表情的人,翻頁。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 會話 ID |
seq | number | 是 | 訊息的序號 |
key | string | 是 | 表情的標識 |
page.cursor | string | null | 否 | 上一頁返回的 next_cursor |
page.limit | number | 否 | 每頁條數,預設 20,最大 100 |
返回值:Promise<{ items: unknown[]; next_cursor: string | null }>。items 的每一項為 { username, nickname, avatar_url, reacted_at },已刪除的使用者 username 為 null。
可能的錯誤:permission_denied(read_only_membership)、rate_limited、not_found。
im.messages.setPinned()
置頂或取消置頂一條訊息。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 會話 ID |
seq | number | 是 | 訊息的序號 |
pinned | boolean | 是 | true 置頂,false 取消 |
返回值:Promise<{ changed: boolean }>。
可能的錯誤:permission_denied(pin_denied、message_recalled、read_only_membership)、limit_exceeded(pinned_message_limit)、not_found。
im.messages.lookup()
按序號從服務端取回訊息,同時儲存到本地。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 會話 ID |
seqs | number[] | 是 | 序號,超過 100 個時 SDK 分批請求 |
返回值:Promise<{ items: MessageView[]; missing: number[] }>,missing 為不存在、已過期或本人看不到的序號。
可能的錯誤:not_found(會話不存在或本人看不到)、network_error。
im.messages.searchLocal()
在本地儲存的文字訊息中查詢。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
keyword | string | 是 | 關鍵詞,按子串匹配,不區分大小寫 |
conversation_id | string | 否 | 只在這個會話中查詢 |
limit | number | 否 | 最多返回的條數,預設 50 |
返回值:Promise<MessageView[]>,按時間倒序。
可能的錯誤:local_validation(reason 為 required)、storage_error。
im.messages.receipts()
查詢群訊息的已讀人數。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 群會話 ID |
seqs | number[] | 是 | 本人傳送的、要求回執的訊息的序號 |
返回值:Promise<ReceiptCount[]>,與 seqs 一一對應,見 ReceiptCount。
可能的錯誤:permission_denied(receipt_denied,本人已離開群)、rate_limited。
im.messages.receiptUsers()
查詢一條群訊息已讀或未讀的人,翻頁。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 群會話 ID |
seq | number | 是 | 訊息的序號 |
p.status | 'read' | 'unread' | 是 | 已讀或未讀的人 |
p.cursor | string | null | 否 | 上一頁返回的 next_cursor |
p.limit | number | 否 | 每頁條數,預設 20,最大 100 |
返回值:Promise<{ items: unknown[]; next_cursor: string | null }>。items 的每一項為 { username, nickname, avatar_url }。
可能的錯誤:permission_denied(receipt_denied)、rate_limited、not_found。
im.messages.getReceipt()
同步讀取記憶體中最近一次得到的已讀人數。變化時發出 receipts.changed。
| 引數 | 型別 | 必填 | 說明 |
|---|---|---|---|
conversation_id | string | 是 | 群會話 ID |
seq | number | 是 | 訊息的序號 |
返回值:ReceiptCount | undefined。
MessageList
im.messages.open() 返回的訊息列表。
| 成員 | 說明 |
|---|---|
conversation_key | 只讀,列表的會話鍵;草稿會話換成正式會話後變為新的會話 ID |
getSnapshot() | 當前的快照,見 MessageListSnapshot |
subscribe(listener) | 快照變化時呼叫 listener(不帶引數),返回取消訂閱的函式 |
loadOlder() | 載入更早的 50 條,返回 Promise<void>;沒有更早的或正在載入時立即返回 |
loadNewer() | 從定位處向下載入,返回 Promise<void>;已在最新處時立即返回 |
loadGap(gap) | 載入一個 skipped 空洞,返回 Promise<void>;rejoin 空洞什麼也不做 |
jumpTo(seq) | 定位到這個序號附近,返回 Promise<void> |
markRead() | 把已讀位置推進到最新一條,合併傳送 |
setViewing({ atBottom?, visible? }) | 報告使用者是否停在最新處、列表是否顯示;省略的欄位不變 |
dispose() | 釋放列表,之後 loadOlder() 等方法以 invalid_state(reason 為 disposed)拒絕 |
載入失敗時方法不拒絕,錯誤放在快照的 error 中。
render 子路徑
從 @deeprespond/im-web/render 匯入。client 引數為 createDRClient() 建立的客戶端,傳入其他物件時丟擲 TypeError。
| 函式 | 引數 | 返回值 |
|---|---|---|
displayName(client, username, opts?) | username:string | null;opts.group_id:取這個群中的群暱稱;opts.locale | string |
conversationTitle(client, view, opts?) | view:ConversationView;opts.locale | string |
summarize(message, opts) | message:至少有 type、body、sender、sender_type、recalled;opts.nameOf:必填;opts.self;opts.locale | string,文字取原文(連續空白合併為一個空格),撤回的為撤回提示,通話記錄給出 self 時為通話的文字 |
renderTip(message, opts) | message:群提示;opts.nameOf:必填;opts.self;opts.locale | string |
renderCallRecord(message, opts) | message:通話記錄;opts.self:必填;opts.locale | string |
renderRecall(message, opts) | message:已撤回的訊息;opts.self、opts.nameOf:必填;opts.locale | string,沒有撤回時為空字串 |
nameOf 的型別為 (username: string) => string,locale 的型別為 Partial<Locale>。
locale/zh-CN 子路徑
從 @deeprespond/im-web/locale/zh-CN 匯入。
| 匯出 | 說明 |
|---|---|
zhCN | 中文文案表:errors(錯誤提示)、summary(摘要)、recall(撤回提示)、tip(群提示)、call(通話記錄),以及 you、deletedUser、unnamedGroup 等條目 |
Locale | 型別,typeof zhCN |
describeError(err, locale?) | 按錯誤碼和原因給出提示文字,見事件與錯誤 |
formatTemplate(template, values) | 把文案中的 {name} 等佔位符替換為 values 中的值 |
資料結構
Message
服務端的訊息物件,欄位的含義見訊息格式。
| 欄位 | 型別 | 說明 |
|---|---|---|
conversation_id | string | 會話 ID |
conversation_type | 'single' | 'group' | 單聊或群聊 |
seq | number | 在會話中的序號 |
message_id | string | 全應用唯一的訊息 ID |
client_msg_id | string | null | 傳送時的去重 ID |
sender | string | null | 傳送者的使用者名稱;系統訊息、傳送者已刪除時為 null |
sender_type | 'user' | 'system' | 使用者或系統 |
recipient | string | null | 單聊的接收者 |
type | string | 訊息型別:text、image、voice、video、file、location、custom、tip、call,之後可能增加 |
body | Record<string, unknown> | null | 訊息體;已撤回、已擦除為 null |
ext | Record<string, unknown> | null | 擴充套件欄位 |
mentions | { usernames: string[] | null; all: boolean } | null | @ 的人 |
reply_to | { seq: number; sender: string | null } | null | 引用的訊息 |
need_receipt | boolean | 是否要求群已讀回執 |
exclude_from_unread | boolean | 是否不計入未讀 |
reactions | Array<{ key, count, users, reacted? }> | null | 表情回應:每種表情一項,users 為前 3 個回應的人(已刪除的為 null) |
pinned | { by: string | null; at: string } | null | 置頂資訊 |
edited | { at: string; count: number } | null | 編輯資訊 |
recalled | { by: string | null; role: string; at: string } | null | 撤回資訊,role 為 sender、owner、admin、server、platform |
erased | boolean | 是否已被擦除 |
via | 'client' | 'openapi' | 'console' | 'system' | 寫入方式 |
created_at | string | 傳送時間 |
message_version | number | 訊息的變更版本號 |
MessageView
訊息列表、send() 返回的訊息:Message 加上本地狀態。
| 欄位 | 型別 | 說明 |
|---|---|---|
| Message 的全部欄位 | 見上表 | |
seq、message_id、created_at | number | null、string | null、string | null | 傳送佇列中的訊息為 null |
conversation_id | string | 發到草稿會話(還沒有服務端會話的單聊)、還沒發出的訊息為 null |
_local.status | 'sent' | 'queued' | 'uploading' | 'sending' | 'failed' | 已發出、等待傳送、上傳中、傳送中、失敗 |
_local.progress | number | 可選,上傳進度 0 到 1,只在 uploading 時有 |
_local.error | DRError | 可選,失敗的原因 |
_local.status_unknown | boolean | 可選,失敗的訊息可能已經發出 |
_local.reacted | Record<string, boolean> | 可選,本人是否回應過各種表情 |
_local.unsupported | boolean | 不認識的訊息型別 |
_local.trusted | boolean | via 不是 client,即不是使用者在客戶端發出的 |
MessageListSnapshot
| 欄位 | 型別 | 說明 |
|---|---|---|
items | readonly MessageView[] | 按序號升序,傳送佇列中的排在最後 |
gaps | readonly Gap[] | 沒有載入的範圍,見 Gap |
hasOlder | boolean | 上面還有更早的訊息 |
hasNewer | boolean | 從某條訊息定位開啟、還沒翻到最新處 |
loading | 'none' | 'initial' | 'older' | 'newer' | 載入狀態 |
pinned | readonly PinnedItem[] | 置頂訊息,見 PinnedItem |
error | DRError | 可選,最近一次載入的錯誤 |
Gap
| 欄位 | 型別 | 說明 |
|---|---|---|
from_seq | number | 起始序號(含) |
to_seq | number | 結束序號(含) |
kind | 'skipped' | 'rejoin' | skipped 離線補齊時跳過的,可以載入;rejoin 重新入群之前看不到的,不能載入 |
PinnedItem
| 欄位 | 型別 | 說明 |
|---|---|---|
seq | number | 訊息的序號 |
pinned | { by: string | null; at: string } | 置頂的人(由服務端置頂時為 null)和時間 |
message | MessageView | null | 訊息,正文還沒取回時為 null |
SendTarget
以下三種之一:
| 形式 | 說明 |
|---|---|
{ conversation_key: string } | 會話鍵,包括草稿會話的 draft:{username} |
{ to_user: string } | 對方的使用者名稱 |
{ to_group: string } | 群 ID |
OutgoingContent
type | 其他欄位 | 說明 |
|---|---|---|
'text' | body: { text: string } | 文字 |
'image' | file: Blob;original?: boolean;body? | 由 SDK 處理、上傳的圖片;original 傳送原圖 |
'voice' | file: Blob;body: { duration_seconds: number } | 由 SDK 上傳的語音,時長必填 |
'video' | file: Blob;body? | 由 SDK 上傳的影片 |
'file' | file: Blob;body?: { name?: string } | 由 SDK 上傳的檔案 |
'image'、'voice'、'video'、'file' | body: Record<string, unknown> | 已有地址的附件,body.url 必填 |
'location' | body: { latitude, longitude, name?, address? } | 位置 |
'custom' | body: Record<string, unknown> | 自定義訊息 |
SendOptions
| 欄位 | 型別 | 說明 |
|---|---|---|
ext | Record<string, unknown> | 擴充套件欄位 |
mentions | { usernames?: string[]; all?: boolean } | @ 的人,只用於群聊 |
reply_to | { seq: number } | 引用的訊息 |
need_receipt | boolean | 要求群已讀回執,只用於群聊 |
exclude_from_unread | boolean | 不計入接收者的未讀數 |
push | { disabled: boolean } | 是否關閉這條訊息的離線推送 |
waitUntil | 'queued' | 'sent' | send() 何時返回,預設 'queued' |
ReceiptCount
以下兩種之一:
| 形式 | 說明 |
|---|---|
{ seq: number; read_count: number; unread_count: number } | 已讀和未讀的人數 |
{ seq: number; code: string; details?: Record<string, unknown> } | 這一條不能檢視,code 為錯誤碼,如 permission_denied(receipt_denied)、not_found |
OnlineMessage
message.online 事件中的訊息。
| 欄位 | 型別 | 說明 |
|---|---|---|
message_id | string | 訊息 ID,只用於排查問題 |
client_msg_id | string | 傳送方生成的 ID |
conversation_type | 'single' | 'group' | 單聊或群聊 |
conversation_id | string | null | 會話 ID,兩人之間還沒有會話時為 null |
sender | string | null | 傳送者 |
sender_type | string | user 或 system |
recipient | string | null | 單聊的接收者 |
group_id | string | null | 群聊的群 ID |
type | string | 總是 custom |
body | Record<string, unknown> | 訊息體 |
ext | Record<string, unknown> | null | 擴充套件欄位 |
via | string | 寫入方式 |
created_at | string | 傳送時間 |
OnlineResult
| 欄位 | 型別 | 說明 |
|---|---|---|
message_id | string | 訊息 ID,只用於排查問題 |
client_msg_id | string | SDK 生成的 ID |
created_at | string | 傳送時間 |
online_only | true | 總是 true |
