k2-im 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1557 @@
1
+ import { Ref, Component } from 'vue';
2
+
3
+ /** IM 领域支持的 5 个展示 locale(与宿主规范 locale 一致)。 */
4
+ type ImLocale = 'en-US' | 'th-TH' | 'vi-VN' | 'zh-CN' | 'zh-TW';
5
+
6
+ /** 媒体业务类型:实现据此选对象存储 bucket/host;sticker 对应 app-sticker。 */
7
+ type MediaKind = 'image' | 'video' | 'voice' | 'file' | 'sticker';
8
+
9
+ /** 会话类型。P0 只用 direct / group。 */
10
+ type ConversationType = 'direct' | 'group' | 'channel' | 'supergroup';
11
+ /**
12
+ * 会话复合身份;`conversationId` 只在对应 `conversationType` 的领域内唯一。
13
+ *
14
+ * @template TType 精确会话类型,供 GROUP/CHANNEL sibling 在编译期隔离。
15
+ * @example
16
+ * ```ts
17
+ * const group: ConversationRef<'group'> = {
18
+ * conversationType: 'group',
19
+ * conversationId: 'group-42',
20
+ * }
21
+ * ```
22
+ */
23
+ interface ConversationRef<TType extends ConversationType = ConversationType> {
24
+ conversationType: TType;
25
+ conversationId: string;
26
+ }
27
+ /** 消息内容类型。sticker=表情/贴图(BMessage contentType=7);call=单聊通话记录(contentType=10);transfer=转账(contentType=16);miniProgram=小程序分享卡(contentType=22);redPacket=红包卡(contentType=25);game=小游戏(contentType=27);未知 wire 正文统一归一为 unknown。 */
28
+ type MessageContentType = 'text' | 'system' | 'image' | 'video' | 'voice' | 'file' | 'sticker' | 'contact' | 'miniProgram' | 'game' | 'transfer' | 'call' | 'redPacket' | 'unknown';
29
+ /**
30
+ * 发送态(客户端态):消息从本地创建到服务端确认的生命周期。
31
+ * `uploading` 仅媒体消息使用:文件上传阶段(uploading→pending→sending→sent);
32
+ * 上传期消息只在 DomainState(不进 Outbox),上传成功拿到 URL 后才交 Outbox 走 sending→sent(Outbox 不感知上传,OutboxStatus 无 uploading)。
33
+ */
34
+ type MessageSendStatus = 'pending' | 'uploading' | 'sending' | 'sent' | 'failed';
35
+ /** 送达/已读态(服务端态):消息在对端的回执状态。 */
36
+ type MessageReceipt = 'unread' | 'delivered' | 'read';
37
+ /** 群系统提示事件类型(结构化语义;UI/i18n 层据此渲染文案,**不在 mapper 写死中文**)。 */
38
+ type ImSystemEventType = 'direct_contact_established' | 'direct_contact_required' | 'direct_message_refused' | 'group_info_changed' | 'group_activated' | 'group_member_joined' | 'group_member_invited' | 'group_member_left' | 'group_member_removed' | 'group_master_changed' | 'group_dismissed' | 'group_member_renamed' | 'group_message_pinned' | 'group_message_unpinned' | 'channel_name_changed' | 'channel_avatar_changed' | 'channel_announcement_changed' | 'channel_member_joined' | 'channel_member_invited' | 'channel_member_left' | 'channel_member_removed' | 'channel_owner_transferred' | 'channel_admin_added' | 'channel_admin_removed' | 'channel_dismissed' | 'channel_all_muted' | 'channel_all_unmuted' | 'channel_created' | 'channel_private_chat_disabled' | 'channel_private_chat_enabled' | 'channel_member_renamed' | 'channel_member_muted' | 'channel_member_unmuted' | 'channel_message_pinned' | 'channel_message_unpinned' | 'channel_unknown' | 'red_packet_claimed' | 'red_packet_claimed_thunder' | 'unknown';
39
+ /** 系统提示结构化内容(contentType==='system'):存语义,不把最终中文句子当主数据源(UI/composable 据 eventType + 资料生成文案)。 */
40
+ interface ImSystemMessageBody {
41
+ eventType: ImSystemEventType;
42
+ /**
43
+ * `hidden` 表示该通知只驱动资料/权限等领域副作用,不作为聊天时间线或会话摘要内容展示。
44
+ * 事件仍会进入 DomainState 并广播,不能把它理解成“忽略通知”。
45
+ */
46
+ timelineVisibility?: 'hidden';
47
+ /** 操作者 userId(如踢人者/改名者/群主)。 */
48
+ operatorId?: string;
49
+ /** 被操作成员 userId 列表(如被踢/退群成员;判断是否含自己以触发受限 side-effect)。 */
50
+ targetUserIds?: string[];
51
+ /** 后端 notifyType 原始值(诊断 / unknown 兜底路由)。 */
52
+ rawNotifyType?: number;
53
+ /** 后端 errcode 原始值(仅非默认时存):申请/邀请结果等边界据此判失败(NON_ERR=0x8000 成功,EXCEPT_ERR=0 为默认未设置)。 */
54
+ errcode?: number;
55
+ /** 后端 sContent 原文(未知 event 或需直出时兜底展示)。 */
56
+ fallbackText?: string;
57
+ /** 结构化扩展(群资料变更 name/desc/head_img_url 等,解析 sContent JSON 后填入)。 */
58
+ payload?: Record<string, unknown>;
59
+ /** 可由产品层执行的结构化动作;当前仅允许 direct 非好友提示触发好友申请。 */
60
+ action?: {
61
+ type: 'request-contact';
62
+ userId: string;
63
+ };
64
+ /** 群 Notify 12/13 的结构化置顶 side-effect;仅 coordinator 消费,不作为最终展示文案或会话摘要持久化。 */
65
+ pinnedMessageAction?: ImPinnedMessageAction;
66
+ }
67
+ /**
68
+ * 会话「最后一条」的语义摘要(持久化、经字段级 lastMessageSummaryEqual 参与判等):取代中文预览串作为语义承重。
69
+ * `system` 分支直接持归一后的 ImSystemMessageBody(供 formatLastMessageSummary 复用 formatSystemMessage,selfId 渲染时传入)。
70
+ * EXPRESSION 协议家族按 `imageType` 投影为 `gif` 或静态 `sticker`,供会话列表保留产品差异。
71
+ * 迁移期与 lastMessagePreview 双写并存(阶段 4);判等**禁比对象引用**(否则 hydrate 回读值/重算值每次新建对象即永不相等)。
72
+ */
73
+ type LastMessageSummary = {
74
+ kind: 'text';
75
+ text: string;
76
+ } | {
77
+ kind: 'image';
78
+ } | {
79
+ kind: 'gif';
80
+ } | {
81
+ kind: 'video';
82
+ } | {
83
+ kind: 'file';
84
+ } | {
85
+ kind: 'sticker';
86
+ } | {
87
+ kind: 'contact';
88
+ } | {
89
+ kind: 'miniProgram';
90
+ } | {
91
+ kind: 'game';
92
+ gameType: ImGameType;
93
+ } | {
94
+ kind: 'transfer';
95
+ } | {
96
+ kind: 'call';
97
+ callKind: ImCallKind;
98
+ } | {
99
+ kind: 'redPacket';
100
+ } | {
101
+ kind: 'unknown';
102
+ } | {
103
+ kind: 'voice';
104
+ durationMs?: number;
105
+ } | {
106
+ kind: 'encrypted';
107
+ } | {
108
+ kind: 'recalled';
109
+ by: string;
110
+ } | {
111
+ kind: 'channel-moderation-deleted';
112
+ by: string;
113
+ } | {
114
+ kind: 'system';
115
+ system: ImSystemMessageBody;
116
+ };
117
+ /**
118
+ * 图片消息内容体(contentType==='image')。领域友好类型(string/number),协议 BMessage 的 bytes/bigint 字段在 mapping 层互转。
119
+ * 对齐 Android IMImageMsgBody / BMessage 图片字段(见 outputs/architecture/subagent-read-docs/研究Android图片消息实现.md §B)。
120
+ */
121
+ interface ImImageBody {
122
+ /** 原图地址:接收/已发 = S3 公网直链(BMessage.content);发送乐观占位期 = 本地 objectURL/blobURL。 */
123
+ url: string;
124
+ /** 原图宽(px,BMessage.width)。 */
125
+ width: number;
126
+ /** 原图高(px,BMessage.height)。 */
127
+ height: number;
128
+ /** 缩略图原始字节(内嵌不单传,对应 BMessage.thumbData bytes,mapping 层零转换直传)。UI 层转 blob/dataURL 渲染(im-core 环境无关,不做 base64)。 */
129
+ thumbData?: Uint8Array;
130
+ /** 原图文件 MD5(BMessage.MD5)。 */
131
+ md5?: string;
132
+ /** 图片类型:0=普通,1=gif(BMessage.imageType)。 */
133
+ imageType?: number;
134
+ /** 文件大小(字节;协议 BMessage.fileLength 单位 KB,mapping 层换算)。 */
135
+ size?: number;
136
+ /** 文件解密 TEA key 原始字节(仅密文图片有;接收端解 BContent 后从 BMessage.teaKey 得,用于解下载到的原图文件)。 */
137
+ teaKey?: Uint8Array;
138
+ /** 文件加密类型:0=跟随消息,1=不加密,3=完整 TEA(BMessage.fileEncryptType,见 Android IMFileEncryptType)。 */
139
+ fileEncryptType?: number;
140
+ }
141
+ /**
142
+ * 视频消息内容体(contentType==='video')。领域友好类型(string/number),协议 BMessage 的 bytes/bigint 字段在 mapping 层互转。
143
+ * 对齐 Android IMVideoMsgBody / BMessage 视频字段(见 outputs/architecture/subagent-read-docs/研究Android视频消息实现.md):
144
+ * 视频 URL 进 BMessage.content、封面=本地首帧截图内嵌 thumbData(不单独上传 S3)、duration 单位秒、原文件整体 TEA 加密(对齐图片走 fileEncryptType=3)。
145
+ */
146
+ interface ImVideoBody {
147
+ /** 视频地址:接收/已发 = S3 公网直链(BMessage.content);发送乐观占位期 = 本地 objectURL/blobURL。 */
148
+ url: string;
149
+ /** 视频像素宽(BMessage.width)。Android 发送侧不写 proto,Web 端补充更严谨(Android 收到会读;缺失兜底 0)。 */
150
+ width: number;
151
+ /** 视频像素高(BMessage.height)。 */
152
+ height: number;
153
+ /** 时长(秒,BMessage.duration;proto 单位秒,contentType=3|4 有效)。 */
154
+ duration?: number;
155
+ /** 封面帧原始字节(本地首帧截图,内嵌不单传,对应 BMessage.thumbData bytes)。UI 转 blob 渲染,无需下载即可展示封面。 */
156
+ thumbData?: Uint8Array;
157
+ /** 视频文件 MD5(BMessage.MD5;Android 当前未赋值,保留可选)。 */
158
+ md5?: string;
159
+ /** 文件大小(字节;协议 BMessage.fileLength 单位 KB,mapping 层换算)。 */
160
+ size?: number;
161
+ /** 视频描述(BMessage.describe,可选)。 */
162
+ describe?: string;
163
+ /** 文件解密 TEA key 原始字节(仅密文视频有;接收端解 BContent 后从 BMessage.teaKey 得,用于解下载到的原视频文件)。 */
164
+ teaKey?: Uint8Array;
165
+ /** 文件加密类型:1=不加密,3=完整 TEA(BMessage.fileEncryptType;视频与图片同体系走完整 TEA)。 */
166
+ fileEncryptType?: number;
167
+ }
168
+ /**
169
+ * 文件(附件)消息内容体(contentType==='file')。领域友好类型(string/number),协议 BMessage 的 bytes/bigint 字段在 mapping 层互转。
170
+ * 对齐 Android IMFileMsgBody / BMessage 文件字段(见 outputs/architecture/subagent-read-docs/研究Android文件上传实现.md):
171
+ * 文件 URL 进 BMessage.content、无缩略图/宽高/时长、文件本体走 simple TEA 加密(fileEncryptType=2,仅前 32 字节,大文件性能优 + 跨端一致)。
172
+ */
173
+ interface ImFileBody {
174
+ /** 文件地址:接收/已发 = S3 公网直链(BMessage.content);发送乐观占位期为空字符串(文件不建本地预览 objectURL)。 */
175
+ url: string;
176
+ /** 原始文件名(含扩展名,如 report.pdf;对应 BMessage.fielName——协议字段名是历史 typo,映射层处理)。 */
177
+ fileName: string;
178
+ /** 文件大小(字节;协议 BMessage.fileLength 对 contentType=11 也按 bytes 直写,mapping 层不换算)。 */
179
+ size?: number;
180
+ /** 扩展名(不含点,如 pdf;BMessage.extension):展示图标 / 拼下载文件名用。 */
181
+ extension?: string;
182
+ /** 文件类型归类(0=未知,1=图片,2=视频,3=语音,4=文档;对齐 Android IMFileType,BMessage.fileType):供对端选图标。 */
183
+ fileType?: number;
184
+ /** MIME(如 application/pdf):仅本地/发送端有(协议 BMessage 无 mime 字段),接收端可能缺失需按 extension 兜底;下载时设 blob type / 决定打开方式。 */
185
+ mimeType?: string;
186
+ /** 文件 MD5(BMessage.MD5;Android 文件发送链路未赋值,保留可选)。 */
187
+ md5?: string;
188
+ /** 文件解密 TEA key 原始字节(仅密文文件有;接收端解 BContent 后从 BMessage.teaKey 得,用于解下载到的原文件)。 */
189
+ teaKey?: Uint8Array;
190
+ /** 文件加密类型(1=不加密,2=simple 前32字节 TEA,3=完整 TEA;BMessage.fileEncryptType)。文件消息默认 2(对齐 Android,大文件性能优)。 */
191
+ fileEncryptType?: number;
192
+ }
193
+ /**
194
+ * 语音消息内容体(contentType==='voice')。领域友好类型(string/number),协议 BMessage 的 bytes/bigint 字段在 mapping 层互转。
195
+ * 对齐 Android IMVoiceMsgBody / BMessage 语音字段(见 outputs/architecture/subagent-read-docs/android-voice-message-analysis.md):
196
+ * 语音 URL 进 BMessage.content、文件为 Speex/Ogg(WB 16k mono)、原文件整体 TEA 加密(fileEncryptType=3,与图片/视频同体系)。
197
+ * ⚠ duration 单位=毫秒:Android `VoiceRecorder.getDuration()` 返回 ms 且 `IMVoiceMsgBody.getPbBytes()` 原样写入 BMessage.duration
198
+ *(接收端 `millisecondToInt` 转秒展示);proto 注释「秒」与真实客户端行为冲突,以 Android 源码/真实样本为准(≠ 视频的秒)。
199
+ */
200
+ interface ImVoiceBody {
201
+ /** 语音文件地址:接收/已发 = S3 公网直链(BMessage.content);发送乐观占位期 = 本地 objectURL。 */
202
+ url: string;
203
+ /** 时长(毫秒,BMessage.duration;Android 语音契约为 ms,展示层自行换算秒)。 */
204
+ durationMs: number;
205
+ /** 语音文件 MD5(BMessage.MD5;Android 语音链路未赋值,保留可选)。 */
206
+ md5?: string;
207
+ /** 文件大小(字节;协议 BMessage.fileLength 对 contentType=4 按 proto 注释单位 KB,mapping 层换算。Android 语音不写此字段,仅 Web 补充)。 */
208
+ size?: number;
209
+ /** 文件解密 TEA key 原始字节(仅密文语音有;接收端解 BContent 后从 BMessage.teaKey 得,用于解下载到的语音文件)。 */
210
+ teaKey?: Uint8Array;
211
+ /** 文件加密类型:0=跟随消息,1=不加密,3=完整 TEA(BMessage.fileEncryptType;语音与图片/视频同走完整 TEA)。 */
212
+ fileEncryptType?: number;
213
+ /** 录音振幅波形(0-255):仅发送端本地展示缓存(随消息落盘、刷新后仍可画波形),**不写入 BMessage**(协议无波形字段)。 */
214
+ waveform?: number[];
215
+ }
216
+ /**
217
+ * 表情 / 贴图消息内容体(contentType==='sticker',协议 BMessage.contentType=7)。
218
+ *
219
+ * @remarks 入站兼容全部 EXPRESSION 子类型;本期主动发送 GIF 固定使用
220
+ * `exprType=2`(LINK)与 `imageType=1`(GIF),并上传原始明文字节到 sticker bucket。
221
+ */
222
+ interface ImStickerBody {
223
+ /** 远端 sticker URL;发送上传期间可暂为宿主创建的本地 object URL。 */
224
+ url: string;
225
+ width: number;
226
+ height: number;
227
+ /** 贴图来源类型;2 表示远端链接。 */
228
+ exprType: number;
229
+ /** 图片子类型;1 表示 GIF,0 表示普通静态贴图。 */
230
+ imageType: number;
231
+ /** 原始文件大小(字节;EXPRESSION 的 fileLength 直接使用 bytes)。 */
232
+ size?: number;
233
+ describe?: string;
234
+ contentId?: string;
235
+ packageName?: string;
236
+ /** 部分历史/内置贴图可能携带缩略图;相册 GIF 主动发送不生成。 */
237
+ thumbData?: Uint8Array;
238
+ }
239
+ /** 小游戏类别;wire `gameType=0` 为石头剪刀布,`gameType=1` 为骰子。 */
240
+ type ImGameType = 'rock-paper-scissors' | 'dice';
241
+ /** 石头剪刀布结果值;具体图片含义由产品 renderer 按同一跨端资源表解释。 */
242
+ type ImRockPaperScissorsValue = 1 | 2 | 3;
243
+ /** 骰子点数。 */
244
+ type ImDiceValue = 1 | 2 | 3 | 4 | 5 | 6;
245
+ /**
246
+ * 小游戏消息正文(contentType==='game',协议 BMessage.contentType=27)。
247
+ *
248
+ * @remarks `gameType` 与 `value` 使用判别联合保持值域一致。发送 API 只接收类型,结果在首次发送时生成并写入
249
+ * Outbox;重试、历史与转发均复用该正文,不能再次随机。
250
+ */
251
+ type ImGameBody = {
252
+ gameType: 'rock-paper-scissors';
253
+ value: ImRockPaperScissorsValue;
254
+ } | {
255
+ gameType: 'dice';
256
+ value: ImDiceValue;
257
+ };
258
+ /**
259
+ * 小程序分享卡展示形态;wire `miniProgramShowType` 只在 mapping 层转换。
260
+ *
261
+ * - `default`:0,普通可点卡片
262
+ * - `board-game`:1,棋牌样式且不可点
263
+ * - `board-game-link`:2,棋牌样式且可点
264
+ * - `unknown`:必须同时保留 `rawShowType`,不得猜成可点/不可点
265
+ */
266
+ type ImMiniProgramShowType = 'default' | 'board-game' | 'board-game-link' | 'unknown';
267
+ /**
268
+ * 小程序分享卡片正文(contentType==='miniProgram',协议 BMessage.contentType=22)。
269
+ *
270
+ * @remarks 字段来自 iOS `IMMiniProgramMessageBody` / proto 61-67、80-82、90-93。
271
+ * `currencySource` 对应 proto field 90(无 miniProgram 前缀):`xj` 现金公司、`xy` 信用公司。
272
+ * `subGameName` 保持服务端 JSON 字符串,不在 Core 解析语种表。打开小程序与 X 币/信用 ID 属于宿主,不进本 DTO。
273
+ */
274
+ interface ImMiniProgramBody {
275
+ /** 小程序应用 id(BMessage.miniProgramAppId)。 */
276
+ appId: string;
277
+ host?: string;
278
+ developerId?: string;
279
+ name?: string;
280
+ icon?: string;
281
+ productId?: string;
282
+ isDynamicHost: boolean;
283
+ suffixAddress?: string;
284
+ showType: ImMiniProgramShowType;
285
+ /** 仅 showType='unknown' 时存在,保证未知展示类型可无损往返。 */
286
+ rawShowType?: number;
287
+ description?: string;
288
+ /** 币种公司来源;棋牌点开据此选择信用/现金会员身份。 */
289
+ currencySource?: string;
290
+ currencyType?: string;
291
+ gamePath?: string;
292
+ /** 多语言子游戏名 JSON 字符串,例如 `{"ch":"...","en":"..."}`。 */
293
+ subGameName?: string;
294
+ }
295
+ /** 转账状态。0 待领取 / 1 已收款 / 2 已退还 / 3 已过期(对齐 iOS IMTranferBody,过期 3 以客户端为准)。 */
296
+ type ImTransferStatus = 0 | 1 | 2 | 3;
297
+ /** 转账会话类型。1 个人 / 2 群(BMessage.sessionType)。 */
298
+ type ImTransferSessionType = 1 | 2;
299
+ /**
300
+ * 转账卡片正文(contentType==='transfer',协议 BMessage.contentType=16)。
301
+ *
302
+ * @remarks 字段来自 iOS `IMTranferBody` / proto 34-50。金额为微单位整数的十进制字符串。
303
+ * 钱包 HTTP 记账属于宿主;本 DTO 只承载会话卡片与状态同步。`transferMsgId` 对应 proto `msgId`(field 47),不是 ImMessage.id。
304
+ */
305
+ interface ImTransferBody {
306
+ fromUid: string;
307
+ toUid: string;
308
+ fromUserName?: string;
309
+ toUserName?: string;
310
+ /** 微单位金额(×1_000_000)的十进制字符串。 */
311
+ amount: string;
312
+ currencyType: number;
313
+ status: ImTransferStatus;
314
+ serialNumber: string;
315
+ remark?: string;
316
+ sessionType: ImTransferSessionType;
317
+ transferTime: number;
318
+ receiveTime?: number;
319
+ returnTime?: number;
320
+ isRead?: boolean;
321
+ transferMsgId?: string;
322
+ coinName?: string;
323
+ coinIcon?: string;
324
+ }
325
+ /** 领域通话媒体类型;已从各端 wire 0/1 对调口径归一,宿主禁止再读 raw callType。 */
326
+ type ImCallKind = 'audio' | 'video';
327
+ /**
328
+ * 通话结束结果。Android 跨端同步把未接与取消都写成 callStatus=0,只能归一为 missed。
329
+ * `unknown` 仅表示该端 status 数字无法识别,不得猜成 completed。
330
+ */
331
+ type ImCallOutcome = 'missed' | 'rejected' | 'cancelled' | 'completed' | 'busy' | 'no-permission' | 'interrupted' | 'unknown';
332
+ /**
333
+ * 单聊通话记录正文(contentType==='call',协议 BMessage.contentType=10)。
334
+ *
335
+ * @remarks JSON 在 BMessage.content。Android 本地 / Android 跨端同步 / iOS 三套字段名与 0/1 含义不同,
336
+ * 只在 mapping/call-record-body 归一。记录由各端本地插入,不能当普通聊天发出。
337
+ */
338
+ interface ImCallRecordBody {
339
+ callKind: ImCallKind;
340
+ outcome: ImCallOutcome;
341
+ /** 通话时长;未接通为 0。Android 本地/同步为毫秒,iOS `content` 为秒,入站已换算。 */
342
+ durationMs: number;
343
+ /** 当前账号是否主叫;JSON 无 isCaller 时省略,不得猜。 */
344
+ outgoing?: boolean;
345
+ }
346
+ /**
347
+ * 红包类型。以客户端运行时为准,**不是** proto 注释:
348
+ * 1 拼手气 / 2 普通 / 3 专属 / 4 踩雷。
349
+ */
350
+ type ImRedPacketType = 1 | 2 | 3 | 4;
351
+ /**
352
+ * 红包卡片正文(contentType==='redPacket',协议 BMessage.contentType=25)。
353
+ *
354
+ * @remarks 字段来自 proto 72-79 + 复用 34/35/38/49。领域名 `packetId` 对应 wire `rad_packet_id`。
355
+ * 金额保持 proto string,单位是否微单位未证实,禁止默认套转账 ×1e6。
356
+ * HTTP 详情状态与本地「自己已领」属于宿主投影,不进本 DTO(status 39 / state 53 未裁定)。
357
+ * 本阶段不提供 `sendRedPacket`;出站字段仅用于 codec 往返闭合。
358
+ */
359
+ interface ImRedPacketBody {
360
+ packetId: string;
361
+ packetType: ImRedPacketType;
362
+ amount?: string;
363
+ count?: string;
364
+ showAmount?: boolean;
365
+ thunderRate?: string;
366
+ isThunder?: boolean;
367
+ thunderNumber?: string;
368
+ fromUid?: string;
369
+ toUid?: string;
370
+ currencyType?: number;
371
+ coinName?: string;
372
+ }
373
+ /** 名片类别。`unknown` 必须同时保留 `rawKind`,不得把未知 wire 值猜成个人/群/频道/机器人。 */
374
+ type ImContactKind = 'individual' | 'group' | 'channel' | 'robot' | 'unknown';
375
+ /**
376
+ * 名片消息内容体(contentType==='contact')。字段来自 BMessage contentType=8,但使用领域友好命名:
377
+ * id/icon/name 分别归一为 targetId/avatar/displayName;visitCardKey 归一为 kind,未知值放 rawKind。
378
+ */
379
+ interface ImContactBody {
380
+ /** 被分享对象 id:个人 userId、普通群 groupId、频道 id 或机器人 id。 */
381
+ targetId: string;
382
+ avatar?: string;
383
+ displayName: string;
384
+ username?: string;
385
+ /** Android 合同:0=其他、1=男、2=女;群/频道名片通常为 0。 */
386
+ gender: number;
387
+ kind: ImContactKind;
388
+ /** 仅 kind='unknown' 时存在,保证未知 visitCardKey 可无损往返。 */
389
+ rawKind?: number;
390
+ }
391
+ /**
392
+ * 未知消息正文的稳定领域语义。
393
+ *
394
+ * @remarks 原始 wire contentType 与 parser 异常只属于 `raw` diagnostics,不进入默认产品 DTO,避免 UI 依赖协议数字或异常文案。
395
+ */
396
+ interface UnknownMessageBody {
397
+ /** 未注册 wire 类型与已注册类型解析失败必须可区分,但两者都不能伪装为空文本。 */
398
+ reason: 'unsupported-content-type' | 'malformed-content';
399
+ }
400
+ /** 按 contentType 的结构化内容体。 */
401
+ interface ImMessageBody {
402
+ /** P0 文本内容(明文,解密后的结果)。 */
403
+ text?: string;
404
+ /** contentType==='system' 时的结构化系统提示体(群通知;text 忽略)。 */
405
+ system?: ImSystemMessageBody;
406
+ /** contentType==='image' 时的图片内容体。 */
407
+ image?: ImImageBody;
408
+ /** contentType==='video' 时的视频内容体。 */
409
+ video?: ImVideoBody;
410
+ /** contentType==='voice' 时的语音内容体。 */
411
+ voice?: ImVoiceBody;
412
+ /** contentType==='file' 时的文件(附件)内容体。 */
413
+ file?: ImFileBody;
414
+ /** contentType==='sticker' 时的表情 / GIF 贴图内容体。 */
415
+ sticker?: ImStickerBody;
416
+ /** contentType==='contact' 时的名片内容体。 */
417
+ contact?: ImContactBody;
418
+ /** contentType==='miniProgram' 时的小程序分享卡片。 */
419
+ miniProgram?: ImMiniProgramBody;
420
+ /** contentType==='game' 时的小游戏结果。 */
421
+ game?: ImGameBody;
422
+ /** contentType==='transfer' 时的转账卡片。 */
423
+ transfer?: ImTransferBody;
424
+ /** contentType==='call' 时的单聊通话记录。 */
425
+ call?: ImCallRecordBody;
426
+ /** contentType==='redPacket' 时的红包卡片。 */
427
+ redPacket?: ImRedPacketBody;
428
+ /** contentType==='unknown' 时的稳定未知正文,不含原始 wire type。 */
429
+ unknown?: UnknownMessageBody;
430
+ }
431
+ /**
432
+ * 群 @ 提及范围段(对应 extend.atRange):`userId='0'`=@所有人段。
433
+ * ⚠️ `start`/`end` 坐标**跨端不可信**(实测部分端相对替换前「@昵称␠」串、与正文 `[userId]` token 错位),**展示不使用**;
434
+ * 仅作协议诊断/兼容字段保留。展示按正文 token + atUserIds/atAll 还原(见 resolveMentionSegments),勿把坐标路径接回。
435
+ */
436
+ interface MentionRange {
437
+ userId: string;
438
+ start: number;
439
+ end: number;
440
+ }
441
+ /**
442
+ * 群消息 @ 提及(Android 三写载体的归一解析):`atAll`(MESGrpChat.nNotifyCount=-1)、`atUserIds`(sNotifyUsers 被 @ 成员,
443
+ * 含正文 token 读边界补全)、`ranges`(extend.atRange,可选、仅诊断/兼容——展示**不依赖其坐标**)。正文 `body.text` 保留 `[userId]` token 原样(真相不变);
444
+ * **展示权威来源 = 正文 token + atUserIds/atAll**,由纯函数 `resolveMentionSegments` 还原为高亮段(含 @昵称/@所有人/@我),UI 不自行解析。
445
+ */
446
+ interface ImMessageMention {
447
+ atAll: boolean;
448
+ atUserIds: string[];
449
+ ranges?: MentionRange[];
450
+ /** extend.atUsersName(userId → 发送端携带的昵称):本地查不到成员名时的**末位兜底**(对齐 Android 昵称优先级最后一档,尤其 @自己/非本地缓存成员)。 */
451
+ atUsersName?: Record<string, string>;
452
+ }
453
+ /**
454
+ * 消息失败机器分类(重试 / fail-fast / 统计消费):**不新增值、不放宽为任意字符串**。
455
+ * `unsupported` = 会话类型未接入发送链路(fail-fast,非重试可解)。展示语义另走 `displayCode`(见下)。
456
+ */
457
+ type ImMessageFailureReason = 'timeout' | 'rejected' | 'encrypt_failed' | 'network' | 'unsupported' | 'unknown';
458
+ /**
459
+ * 失败**展示语义**码(强类型枚举,非裸串):供 im-i18n 按 locale 渲染失败气泡文案。
460
+ * 由 `reason` / `SendMediaFailReason` / ACK errcode 穷尽映射而来,与 im-i18n failure catalog 键一一对应(media.* 带 mediaKind 参数)。
461
+ */
462
+ type ImFailureDisplayCode = 'media.empty' | 'media.noCrypto' | 'media.previewFailed' | 'media.encryptFailed' | 'media.uploadFailed' | 'media.cancelled' | 'media.invalidDuration' | 'media.uploadInterrupted' | 'contactRequired' | 'messageRefused' | 'forbiddenOrLeft' | 'contentRejected' | 'groupDismissed' | 'groupUnavailable' | 'channelDismissed' | 'channelMuted' | 'channelNotMember' | 'channelAdminRequired' | 'conversationUnsupported' | 'sendCanceled' | 'sessionLoggedOut' | 'timeout' | 'network' | 'unknown';
463
+ interface ImMessageFailure {
464
+ /** 归一后的错误码(来自 MESChatAck.errcode 或本地失败码,本地码用负值)。 */
465
+ code: number;
466
+ /** 失败机器分类(重试 / fail-fast / 统计消费)。 */
467
+ reason: ImMessageFailureReason;
468
+ /** 失败展示语义码(强类型):UI 经 im-i18n 按 locale 渲染;缺省表示无专门展示码(UI 回退 message)。不参与去重判等。 */
469
+ displayCode?: ImFailureDisplayCode;
470
+ /** 展示模板参数(如未支持会话类型的 type);不参与去重判等。 */
471
+ params?: Record<string, string | number>;
472
+ /** 媒体失败的媒体类型(图片/视频/语音/文件):供 displayCode 为 media.* 时渲染 label。 */
473
+ mediaKind?: MediaKind;
474
+ /** 后端原文 / 诊断兜底文案;仅诊断/展示兜底用,不参与去重判等(见 merge.failureEqual)。 */
475
+ message?: string;
476
+ /**
477
+ * 是否可重试(false=业务级不可重试):如 direct 非好友或 GROUP 不在群(0x8201)、内容为空/违规(0x8202)、群已解散(0x8315)→ UI 隐藏「重发」。
478
+ * 缺省(undefined)视为可重试(向后兼容)。由 code 决定(classifyAckFailure),故不参与去重判等(同 code 必同 retryable)。
479
+ */
480
+ retryable?: boolean;
481
+ }
482
+ interface ImMessageEncryption {
483
+ /** P0 true=消息密文链路, false=明文链路(encrypt=0)。 */
484
+ encrypted: boolean;
485
+ /** 扩展:加密方案标识,默认 'rsa-tea'。 */
486
+ scheme?: 'rsa-tea';
487
+ /** true=密文但解密失败/缺密钥(占位,非真实空正文):供 UI 显式「无法解密」提示,与明文空消息区分。 */
488
+ decryptFailed?: boolean;
489
+ }
490
+ /** 单条消息领域模型(§4.1)。主键为客户端生成的 id(无 serverId)。 */
491
+ interface ImMessage {
492
+ /** P0 客户端生成的稳定消息 ID(UUID),同时作幂等键;刷新后保持。 */
493
+ id: string;
494
+ /** P0 归一化会话 ID。 */
495
+ conversationId: string;
496
+ conversationType: ConversationType;
497
+ /** P0 归一化发送者 ID。 */
498
+ senderId: string;
499
+ contentType: MessageContentType;
500
+ body: ImMessageBody;
501
+ /** P0 统一毫秒 number(本地创建时间,乐观渲染用)。 */
502
+ createdAt: number;
503
+ /** P0 SDK 内部稳定排序键(见 §6.3),宿主只用于排序、不解析语义。 */
504
+ sortKey: string;
505
+ status: MessageSendStatus;
506
+ /** 失败原因(status==='failed' 时)。 */
507
+ failure?: ImMessageFailure;
508
+ /** P0 服务端回传时间(归一为 number)。 */
509
+ serverTime?: number;
510
+ /** P0 默认 'unread'。 */
511
+ receipt: MessageReceipt;
512
+ encryption: ImMessageEncryption;
513
+ /** 群消息 @ 提及(仅群 @ 消息有;解析自 MESGrpChat 明文字段 + extend)。展示走 resolveMentionSegments。 */
514
+ mention?: ImMessageMention;
515
+ /** 扩展:回复。 */
516
+ refMessageId?: string;
517
+ /** 扩展。 */
518
+ edited?: boolean;
519
+ /**
520
+ * 撤回态:撤回者 id(`by`)+ 撤回时间毫秒(`at`)。存在即「已撤回」——正文清空、UI 显示撤回占位气泡(对齐 Android isCancel=1)。
521
+ * 撤回者=自己 → 展示「你撤回了一条消息」;=对端/群成员 → 展示「对方/成员名 撤回了一条消息」。
522
+ */
523
+ recalled?: {
524
+ by: string;
525
+ at: number;
526
+ };
527
+ /**
528
+ * CHANNEL 管理员/社群主全员删除终态。它与发送者本人撤回、`deleteLocal` 完全不同:服务端通过
529
+ * `RADIO_ADMIN_CANCEL_*` 同步给全员,正文不可逆清空,产品统一展示“管理员删除了一条消息”。
530
+ */
531
+ deletedForEveryone?: {
532
+ by: string;
533
+ at: number;
534
+ reason: 'channel-moderation';
535
+ };
536
+ /** 消息表情回应的领域真相;事件日志只留在 Core,SDK 产品 DTO 仅投影聚合结果。 */
537
+ reactions?: ImMessageReactionState;
538
+ /** 扩展位:后端新增语义先落这里,避免污染核心字段。 */
539
+ extensions?: Record<string, unknown>;
540
+ /**
541
+ * 诊断快照(§4.1 诊断口径):后端下发的原始协议字段(已归一为可 JSON 序列化形态)。
542
+ * 默认不填充,仅当映射开启 keepRaw 时附带,便于联调、与后端核对原始下发;不参与任何业务逻辑。
543
+ */
544
+ raw?: ImMessageRaw;
545
+ }
546
+ /** Reaction wire action:0 添加/替换,1 移除。 */
547
+ type ImReactionAction = 0 | 1;
548
+ /** 单条 Reaction 控制事件;它不属于消息时间线。 */
549
+ interface ImReactionEvent {
550
+ id: string;
551
+ conversation: ConversationRef<'direct' | 'group' | 'channel'>;
552
+ parentMessageId: string;
553
+ senderId: string;
554
+ emoji: string;
555
+ action: ImReactionAction;
556
+ createdAt: number;
557
+ }
558
+ /** 父消息上的一个表情聚合。 */
559
+ interface ImReactionAggregate {
560
+ emoji: string;
561
+ count: number;
562
+ mine: boolean;
563
+ latestAt: number;
564
+ participantIds: readonly string[];
565
+ }
566
+ /** Core 持久化状态;`events` 用于去重、乱序和乐观回滚,不进入普通 SDK 产品出口。 */
567
+ interface ImMessageReactionState {
568
+ version: number;
569
+ total: number;
570
+ currentUserEmoji?: string;
571
+ events: readonly ImReactionEvent[];
572
+ aggregates: readonly ImReactionAggregate[];
573
+ }
574
+ /** 置顶消息可展示快照:完整消息可直接复用既有渲染;不支持的内容显式保留部分预览,不伪造空消息。 */
575
+ type ImPinnedMessagePreview = {
576
+ state: 'ready';
577
+ message: ImMessage;
578
+ } | {
579
+ state: 'partial';
580
+ senderId?: string;
581
+ createdAt?: number;
582
+ previewText?: string;
583
+ reason: 'missing-content' | 'unsupported-content';
584
+ };
585
+ /**
586
+ * 单会话当前置顶消息。它与 `ImSession.pinned`(会话列表本地 sticky 偏好)是两套独立语义:
587
+ * 本结构描述服务端 board 的 0/1 条消息置顶真相,主键为 conversationId。
588
+ */
589
+ interface ImPinnedMessage {
590
+ conversationId: string;
591
+ conversationType: Extract<ConversationType, 'group' | 'channel' | 'supergroup'>;
592
+ messageId: string;
593
+ preview: ImPinnedMessagePreview;
594
+ pinnedAt?: number;
595
+ operatorId?: string;
596
+ }
597
+ type ImPinnedMessageActionErrorCode = 'INVALID_BASE64' | 'INVALID_JSON' | 'INVALID_ENVELOPE' | 'INVALID_MESSAGE_CONTENT' | 'CONVERSATION_MISMATCH' | 'INVALID_SNAPSHOT';
598
+ /** Notify 12/13 经 mapper 产出的结构化动作;坏 payload 显式保留错误码,不能等价为 unpin。 */
599
+ type ImPinnedMessageAction = {
600
+ type: 'pin';
601
+ pinnedMessage: ImPinnedMessage;
602
+ } | {
603
+ type: 'unpin';
604
+ } | {
605
+ type: 'invalid';
606
+ code: ImPinnedMessageActionErrorCode;
607
+ };
608
+ /**
609
+ * 后端原始协议字段诊断快照(§4.1)。仅供调试 / 联调 / 与后端核对原始数据,默认不填充。
610
+ * 所有大整数已转 string,保证整体可 JSON.stringify。
611
+ */
612
+ interface ImMessageRaw {
613
+ /** 来源命令字(如 MES_CHAT_DELIVER)。 */
614
+ cmdId?: number;
615
+ /** 协议层 MESChat 关键字段(sFromId/sToId/msgTime 等大整数已转 string)。 */
616
+ chat?: {
617
+ sMsgId: string;
618
+ sFromId: string;
619
+ sToId: string;
620
+ msgType: number;
621
+ encrypt: number;
622
+ msgTime: string;
623
+ extend?: string;
624
+ encryptVersion?: string;
625
+ parentMsgId?: string;
626
+ };
627
+ /** 解码后的内容层 BMessage 关键字段(明文链路)。 */
628
+ content?: {
629
+ contentType: number;
630
+ text?: string;
631
+ };
632
+ /** 内容层无法按预期 codec 解码时的非敏感诊断;不保留原始正文,避免日志或持久化泄露消息内容。 */
633
+ contentDecodeFailure?: {
634
+ codec: 'BMessage';
635
+ byteLength: number;
636
+ };
637
+ /** CHANNEL RadioNotify 诊断字段;17/18/22/23/24 也只保留在这里,不产生 pin/权限副作用。 */
638
+ channelNotify?: {
639
+ notifyType: number;
640
+ historyCursor?: string;
641
+ sequence?: string;
642
+ extend?: string;
643
+ };
644
+ }
645
+ /**
646
+ * 群会话本地受限状态(退群/被踢/解散,来自群通知 side-effect):驱动禁发 / 停止拉历史 / 清群密钥缓存。仅 group。
647
+ * 'active'=正常、'left'=自己退群、'removed'=被踢、'dismissed'=群解散、'unknown_restricted'=发送 ACK 报权限错但无通知兜底。
648
+ */
649
+ type GroupSessionState = 'active' | 'left' | 'removed' | 'dismissed' | 'unknown_restricted';
650
+ /** CHANNEL 成员/访问状态。与普通群 `GroupSessionState` 独立,禁止互相转换或复用权限语义。 */
651
+ type ChannelSessionState = 'active' | 'guest' | 'left' | 'removed' | 'dismissed' | 'muted' | 'blacklisted' | 'unknown_restricted';
652
+ /** 会话摘要领域模型(§4.2)。 */
653
+ interface ImSession {
654
+ id: string;
655
+ type: ConversationType;
656
+ /** P0 由映射层从用户/群资料补齐。 */
657
+ title: string;
658
+ avatar?: string;
659
+ /**
660
+ * 会话最后一条的预览串(**已收窄**,阶段 4c):仅承载 text 消息的正文(供会话列表 previewSegments 解析 @/表情,需原始正文串);
661
+ * 媒体 / 加密 / 系统 / 撤回一律空串——其语义改由 lastMessageSummary 承载、UI 经 im-i18n 渲染。旧盘遗留的中文占位在 hydrate / projectSession 重算时清空。
662
+ */
663
+ lastMessagePreview: string;
664
+ /**
665
+ * 最后一条消息的语义摘要(持久化):取代中文 lastMessagePreview 作为语义承重,UI 经 im-i18n 按 locale 渲染。
666
+ * 迁移期与 lastMessagePreview 双写并存(阶段 4);判等用字段级 lastMessageSummaryEqual(禁比对象引用)。旧会话无此字段时 UI 回退 lastMessagePreview。
667
+ */
668
+ lastMessageSummary?: LastMessageSummary;
669
+ /** 最后一条消息发送者 id(群会话列表预览「发送者名: 内容」前缀用;`'system'`=系统提示、==自己=我发的,前缀策略由宿主定)。 */
670
+ lastMessageSenderId?: string;
671
+ /** 最后一条消息若为 system(群通知)的事件类型(会话列表据此生成系统文案,避免通知 sContent 空时显「暂无消息」)。 */
672
+ lastMessageSystemEvent?: ImSystemEventType;
673
+ /** 最后一条 system 消息的 targetUserIds 是否含当前用户(会话列表据此区分「你被移出/加入群聊」vs「有成员…」)。派生展示字段、未持久化。 */
674
+ lastMessageSystemTargetsSelf?: boolean;
675
+ /** 最后一条消息的 @ 提及(会话列表预览解析 @:token→@昵称、@所有人 本地化,与消息线程一致)。派生展示字段、未持久化。 */
676
+ lastMessageMention?: ImMessageMention;
677
+ /** 最后一条消息若已撤回:撤回者 id(会话列表预览显示「XXX撤回了一条消息」,展示名/文案由宿主按 by 生成)。派生展示字段、未持久化。 */
678
+ lastMessageRecalled?: {
679
+ by: string;
680
+ };
681
+ /** 最后一条 CHANNEL 消息若被管理员全员删除;与本人撤回分开投影。派生展示字段、未持久化。 */
682
+ lastMessageDeletedForEveryone?: {
683
+ by: string;
684
+ };
685
+ /**
686
+ * 会话「未读的 @我」条数(别人 @我 且未读,跨消息累计——**非仅最后一条**):>0 时会话列表展示「[有人@我]」角标,
687
+ * 会话内容仍显示最新一条消息。收到别人 @我 +1、查看/已读该会话清零。投影自 ReadState.mentionUnreadCount,与 unreadCount 同源持久化。
688
+ */
689
+ mentionUnreadCount?: number;
690
+ /** P0 排序时间(毫秒 number)。 */
691
+ timestamp: number;
692
+ /**
693
+ * 用户手工添加的本地“稍后处理”提醒。
694
+ *
695
+ * 它不回退消息级已读游标,也不改变真实 `unreadCount`;产品列表在真实未读为 0 时将其投影为角标 1。
696
+ * 该字段只由 `ReadState.manuallyUnread` 派生,不作为独立持久化真相。
697
+ */
698
+ manuallyUnread?: boolean;
699
+ unreadCount: number;
700
+ pinned: boolean;
701
+ muted: boolean;
702
+ /** 置顶时间戳(毫秒;pinned=true 时写入,多置顶按此降序——后置顶更靠前)。本地偏好、随会话持久化。 */
703
+ topTime?: number;
704
+ /**
705
+ * 最新一条消息若为「己方且未完成发送」的态:'sending'(pending/uploading/sending 统一)/ 'failed'。
706
+ * 派生展示字段(随最新消息变化,会话列表据此显示发送中 loading / 失败图标);运行期真相,不持久化(persistSession 落盘时剔除)。
707
+ */
708
+ lastMessageSendState?: 'sending' | 'failed';
709
+ /** P0 我方已读游标(见 §4.3)。 */
710
+ readCursor?: string;
711
+ /** 群受限状态(仅 group;来自 0x2204 群通知 side-effect)。缺省视为 'active'(可正常发送)。 */
712
+ groupState?: GroupSessionState;
713
+ /** CHANNEL 独立受限状态;不得写入 `groupState`。缺省表示频道摘要/资料尚未同步。 */
714
+ channelState?: ChannelSessionState;
715
+ /** CHANNEL profile 投影的 capability;仅 `type='channel'` 消费,GROUP/direct 必须忽略。 */
716
+ channelPermissions?: ChannelPermissionSet;
717
+ /** CHANNEL profile 投影的时变访问事实;普通 ACK/通知只改状态时不得伪造。 */
718
+ channelAccessSnapshot?: ChannelAccessSnapshot;
719
+ /** 发送被禁用原因(受限时 UI 禁用输入 + 展示原因);无则可正常发送。 */
720
+ sendDisabledReason?: string;
721
+ extensions?: Record<string, unknown>;
722
+ }
723
+ /** CHANNEL 角色的领域语义;后端未知值必须映射为 `unknown` 并保留在诊断层。 */
724
+ type ChannelRole = 'owner' | 'admin' | 'member' | 'guest' | 'unknown';
725
+ /** CHANNEL 非时变生命周期状态;`muted` 只能由访问快照按当前时间派生,不能作为基础事实保存。 */
726
+ type ChannelBaseSessionState = Exclude<ChannelSessionState, 'muted'>;
727
+ /**
728
+ * CHANNEL 可用能力快照。字段表示客户端当前能否展示/启用动作,服务端仍是最终权限权威。
729
+ * 未得到证据的能力保持 `undefined`,不得按 GROUP 角色规则推导。
730
+ */
731
+ interface ChannelPermissionSet {
732
+ sendMessages: boolean;
733
+ mentionAll?: boolean;
734
+ editProfile?: boolean;
735
+ manageMembers?: boolean;
736
+ manageAdmins?: boolean;
737
+ moderateMessages?: boolean;
738
+ manageMute?: boolean;
739
+ dismissChannel?: boolean;
740
+ transferOwnership?: boolean;
741
+ }
742
+ /**
743
+ * CHANNEL 资料映射时保存的访问限制事实。
744
+ *
745
+ * `baseState` 只承载成员关系/生命周期等非时变事实;`resolvedState` 是本快照上次映射时得到的状态,
746
+ * 运行期若 session 状态与它不同,说明后续 ACK/通知已覆盖旧资料,此快照不得反向覆盖新状态。
747
+ * 该结构只允许保存权限与禁言事实,严禁写入 `secret_key` 或其它密钥正文。
748
+ */
749
+ interface ChannelAccessSnapshot {
750
+ /** 当前账号在 CHANNEL 内的角色;未知值必须为 `unknown`。 */
751
+ role: ChannelRole;
752
+ /** 不包含时变禁言的资料生命周期状态。 */
753
+ baseState: ChannelBaseSessionState;
754
+ /** 资料映射/最近一次 hydrate 重算后的状态,用于识别后续通知覆盖。 */
755
+ resolvedState: ChannelSessionState;
756
+ /** 服务端是否开启全员禁言;有时段时只在时段内生效。 */
757
+ allMembersMuted: boolean;
758
+ /** 当前账号是否命中个人禁言名单;不受全员禁言时段影响。 */
759
+ currentUserMuted: boolean;
760
+ /** Android 合同的本地自然日 `HH:mm_HH:mm` 时段;非法或跨午夜时段不命中。 */
761
+ mutePeriod?: string;
762
+ }
763
+
764
+ interface ImAuthIssue {
765
+ code: number;
766
+ reason: 'token_expired' | 'kicked' | 'banned' | 'unknown';
767
+ }
768
+
769
+ /**
770
+ * Core 结构化诊断合同与中央安全过滤。
771
+ * writer 只接收复制后的业务快照,永久排除认证凭据、密钥、签名材料和二进制;诊断默认关闭且不参与决策。
772
+ */
773
+
774
+ /** 诊断级别按严重度从高到低排列;配置级别表示允许输出的最低严重度。 */
775
+ type ImDiagnosticLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
776
+ /** 诊断产生层;同一 operation 可跨层复用 traceId/operationId。 */
777
+ type ImDiagnosticLayer = 'sdk' | 'core' | 'chat-kit' | 'pinia' | 'worker';
778
+ /** 稳定诊断分类。分类用于构造 payload 前过滤,不能作为业务状态分支。 */
779
+ type ImDiagnosticCategory = 'session' | 'connection' | 'protocol' | 'sync' | 'conversation' | 'history' | 'message' | 'outbox' | 'draft' | 'search' | 'media' | 'profile' | 'storage' | 'crypto';
780
+ /** 经过安全过滤的错误快照;不会保留原始 cause 引用。 */
781
+ interface ImDiagnosticError {
782
+ name?: string;
783
+ message: string;
784
+ code?: string | number;
785
+ category?: string;
786
+ retryable?: boolean;
787
+ stack?: string;
788
+ data?: unknown;
789
+ }
790
+ /** SDK/Core/Chat Kit/Worker 共用的结构化诊断记录。 */
791
+ interface ImDiagnosticRecord<TMessage = ImMessage> {
792
+ timestamp: number;
793
+ sequence: number;
794
+ level: ImDiagnosticLevel;
795
+ layer: ImDiagnosticLayer;
796
+ category: ImDiagnosticCategory;
797
+ event: string;
798
+ traceId?: string;
799
+ operationId?: string;
800
+ generation?: number;
801
+ durationMs?: number;
802
+ accountId?: string;
803
+ conversation?: ConversationRef;
804
+ /** 产品会话或 handle 快照;写入 sink 前会复制并执行安全过滤。 */
805
+ conversationSnapshot?: unknown;
806
+ message?: TMessage;
807
+ error?: ImDiagnosticError;
808
+ data?: unknown;
809
+ }
810
+ /** 自定义 sink 只能收到中央安全过滤后的 record;抛错不会改变业务结果。 */
811
+ interface ImDiagnosticSink$1<TMessage = ImMessage> {
812
+ write: (record: ImDiagnosticRecord<TMessage>) => void;
813
+ }
814
+
815
+ /**
816
+ * 后端协议的目标服务命名。
817
+ *
818
+ * `core`、`wallet`、`bi` 沿用当前 H5/BFF 已接入的服务分组;`im` 用于后续 IM
819
+ * HTTP 业务接口直连。调用方应优先通过 `target` 切换服务,而不是在业务代码里手动拼接
820
+ * baseURL 或 prefix,这样未来从 BFF 迁移到客户端直连时可以只调整运行时配置。
821
+ */
822
+ type ApiProtocolTarget = 'core' | 'wallet' | 'bi' | 'im';
823
+ /**
824
+ * 上传进度事件的协议层抽象。
825
+ *
826
+ * 默认 axios transport 会从 AxiosProgressEvent 映射到该结构;如果未来接入原生容器网络层,
827
+ * 也只需要按相同字段回调业务层即可。
828
+ */
829
+ interface ApiProtocolUploadProgress {
830
+ /** 已上传字节数。 */
831
+ loaded: number;
832
+ /** 总字节数;部分运行时无法拿到时为空。 */
833
+ total?: number;
834
+ /** 0 到 1 之间的上传比例;无法计算时为空。 */
835
+ progress?: number;
836
+ /** 底层 transport 的原始进度事件,供调试或特殊场景读取。 */
837
+ event?: unknown;
838
+ }
839
+ /**
840
+ * 单次协议请求的可覆盖项。
841
+ *
842
+ * 默认请求会按当前 H5/BFF 协议自动补公共参数、签名、AES 加密、token、语言和设备头。
843
+ * 只有上传、特殊回调或兼容接口这类非标准请求,才需要显式传 `body`、关闭 `encrypt/sign`
844
+ * 或使用 `customUrl`。
845
+ */
846
+ interface ApiProtocolRequestOptions {
847
+ /** HTTP method,默认 `POST`;`GET` 请求会把协议参数拼到 query。 */
848
+ method?: string;
849
+ /** 目标后端服务,默认 `core`。IM 业务请求应传 `im`。 */
850
+ target?: ApiProtocolTarget;
851
+ /** 业务参数。数字会按现有协议转换为字符串,避免大整数在 JSON 中丢精度。 */
852
+ data?: Record<string, unknown>;
853
+ /** 自定义请求体。传入后默认不做协议加密/签名,适合 FormData 或已编码内容。 */
854
+ body?: BodyInit | null;
855
+ /** 追加或覆盖请求头;需要特殊端类型等场景可覆盖公共协议头。 */
856
+ headers?: HeadersInit;
857
+ /** 覆盖目标服务默认 prefix。 */
858
+ prefix?: string;
859
+ /** 覆盖目标服务默认 baseURL,常用于灰度环境或临时调试。 */
860
+ baseUrl?: string;
861
+ /** 覆盖默认渠道 ID。 */
862
+ channelId?: string;
863
+ /** 是否对普通请求体执行 AES 加密;默认在无自定义 body 时开启。 */
864
+ encrypt?: boolean;
865
+ /** 是否生成协议签名;默认在无自定义 body 时开启。 */
866
+ sign?: boolean;
867
+ /** 客户端超时时间,默认 60 秒。 */
868
+ timeoutMs?: number;
869
+ /** 覆盖依赖注入中的 token 读取结果;传空字符串可主动发起未登录请求。 */
870
+ token?: string | null;
871
+ /** 覆盖依赖注入中的 locale 读取结果。 */
872
+ locale?: string | null;
873
+ /** 覆盖浏览器 userAgent 派生逻辑,主要用于测试或容器环境。 */
874
+ userAgent?: string | null;
875
+ /** `true` 时 `path` 被视为完整 URL,不再拼接 target baseURL/prefix。 */
876
+ customUrl?: boolean;
877
+ /** 外部取消信号,会和内部 timeout 信号合并。 */
878
+ signal?: AbortSignal;
879
+ /** 上传进度回调;默认浏览器客户端的 axios transport 支持该能力。 */
880
+ onUploadProgress?: (progress: ApiProtocolUploadProgress) => void;
881
+ /**
882
+ * 响应解析模式。
883
+ * - `'json'`(默认):按 content-type 走 JSON / 文本 / AES-JSON 解析,返回业务响应对象。
884
+ * - `'raw'`:原样返回响应字节(`data` 为 `Uint8Array`),不做 JSON / 文本解析;用于
885
+ * protobuf 等二进制接口(如 IM CM 离线消息)。通常与二进制 `body` 搭配使用。
886
+ */
887
+ responseMode?: 'json' | 'raw';
888
+ }
889
+ /**
890
+ * 后端业务响应体。
891
+ *
892
+ * @template T 业务数据 `data` 的类型;当接口无数据或失败时,`data` 可能为空。
893
+ */
894
+ interface ApiProtocolResponse<T = unknown> {
895
+ code: number;
896
+ data?: T | null;
897
+ msg?: string;
898
+ time?: number;
899
+ [key: string]: unknown;
900
+ }
901
+ /**
902
+ * 协议客户端函数。
903
+ *
904
+ * @template T 业务响应 `data` 类型。
905
+ * @example
906
+ * ```ts
907
+ * const response = await client<{ nickname: string }>('/v1/user/profile', {
908
+ * target: 'im',
909
+ * data: { userId: '10001' },
910
+ * });
911
+ * ```
912
+ */
913
+ type ApiProtocolClient = <T = unknown>(path: string, options?: ApiProtocolRequestOptions) => Promise<ApiProtocolResponse<T>>;
914
+
915
+ /** 单个媒体类型的对象存储位置:bucket + 公网 host(URL = host/key)。 */
916
+ interface MediaBucketConfig {
917
+ bucket: string;
918
+ host: string;
919
+ }
920
+ interface MediaTransferAdapterConfig {
921
+ /** AWS Cognito 身份池 id(形如 `ap-northeast-1:xxxx`;region 从前缀解析)。 */
922
+ identityPoolId: string;
923
+ /** 群、社群与用户头像使用的独立 app-header bucket/host;不得复用聊天图片桶。 */
924
+ avatar?: MediaBucketConfig;
925
+ /** 各媒体类型的 bucket/host 映射(对齐 Android:image→app-image 等)。 */
926
+ buckets: Record<MediaKind, MediaBucketConfig>;
927
+ /** 是否启用 S3 Transfer Acceleration(对齐 Android,默认 true)。 */
928
+ useAccelerateEndpoint?: boolean;
929
+ /**
930
+ * 媒体发送阶段的并发上限;由 im-core 消费,adapter 本身仍只负责单个上传任务。
931
+ * 缺省为 4,允许 1-6;高并发会提高吞吐,也会增加文件加密的内存峰值。
932
+ */
933
+ maxConcurrentUploads?: number;
934
+ }
935
+
936
+ /**
937
+ * 将 Core 消息、正文和失败原因投影成 default 产品可展示模型。
938
+ *
939
+ * 映射保留业务正文、发送状态和精确会话身份,删除 sortKey、协议 raw、extensions、密钥与内部 cause;
940
+ * 未知正文显式投影为 `unknown`,不能猜成文本。这里也集中生成稳定 SDK 错误,供所有产品域统一暴露。
941
+ */
942
+
943
+ /** 一种表情回应的稳定聚合;不包含内部事件 ID 或协议字段。 */
944
+ interface ImMessageReactionAggregate {
945
+ emoji: string;
946
+ count: number;
947
+ mine: boolean;
948
+ latestAt: number;
949
+ participantIds: readonly string[];
950
+ }
951
+ /** 父消息上的 Reaction 产品快照。version 是本地变更序号,不是服务端版本。 */
952
+ interface ImMessageReactions {
953
+ version: number;
954
+ total: number;
955
+ currentUserEmoji?: string;
956
+ aggregates: readonly ImMessageReactionAggregate[];
957
+ }
958
+ /** 可渲染媒体正文;密钥和底层文件加密策略只允许留在 SDK/Core。 */
959
+ interface ImMessageViewBody extends Omit<ImMessageBody, 'image' | 'video' | 'voice' | 'file' | 'sticker'> {
960
+ image?: Readonly<Omit<NonNullable<ImMessageBody['image']>, 'teaKey' | 'fileEncryptType'>>;
961
+ video?: Readonly<Omit<NonNullable<ImMessageBody['video']>, 'teaKey' | 'fileEncryptType'>>;
962
+ voice?: Readonly<Omit<NonNullable<ImMessageBody['voice']>, 'teaKey' | 'fileEncryptType'>>;
963
+ file?: Readonly<Omit<NonNullable<ImMessageBody['file']>, 'teaKey' | 'fileEncryptType'>>;
964
+ sticker?: Readonly<NonNullable<ImMessageBody['sticker']>>;
965
+ }
966
+ /**
967
+ * 官方 Chat Kit 与宿主消息线程使用的完整稳定消息 DTO。
968
+ *
969
+ * @remarks 保留正文、mention、回复、编辑、撤回、管理员删除、回执、加密和稳定失败语义;永久排除
970
+ * `sortKey`、原始协议 `raw`、包含 history cursor 的 `extensions` 以及媒体 key。定位和下载必须继续
971
+ * 通过绑定精确 `ConversationRef` 的 SDK handle/capability 完成,UI 不得从 DTO 恢复内部字段。
972
+ */
973
+ type ImMessageView = Readonly<Omit<ImMessage, 'sortKey' | 'raw' | 'extensions' | 'body' | 'reactions'> & {
974
+ body: ImMessageViewBody;
975
+ reactions?: ImMessageReactions;
976
+ }>;
977
+
978
+ /**
979
+ * 将 Core 会话列表投影成 default 产品 DTO 和可订阅原子快照。
980
+ *
981
+ * 列表仍以当前账号 DomainState 为唯一真相;本层删除协议 cursor、extensions 和内部权限原子,保留宿主
982
+ * 展示与动作所需字段。direct/GROUP/CHANNEL 分别校验,刷新失败进入 error 快照,不能用空列表冒充成功。
983
+ */
984
+
985
+ /** default 会话列表支持的精确类型;`supergroup` 继续受产品 Gate 约束。 */
986
+ type ProductConversationListType = 'direct' | 'group' | 'channel';
987
+
988
+ type ConversationEntryUnreadSnapshot = Readonly<{
989
+ state: 'none';
990
+ }> | Readonly<{
991
+ state: 'located';
992
+ firstMessageId: string;
993
+ count?: number;
994
+ }> | Readonly<{
995
+ state: 'unavailable';
996
+ }>;
997
+
998
+ /**
999
+ * SDK 对外的结构化诊断投影、过滤与 sink 装配。
1000
+ *
1001
+ * 记录会复制允许的会话/消息业务字段,永久排除 token、Authorization/Cookie、设备凭据、私钥、
1002
+ * session/TEA key、签名地址和原始二进制。诊断默认关闭,只用于观测,任何 record 都不得参与业务决策或持久化。
1003
+ */
1004
+
1005
+ type ImDiagnosticImageBody = Omit<ImImageBody, 'thumbData' | 'teaKey' | 'fileEncryptType'>;
1006
+ type ImDiagnosticVideoBody = Omit<ImVideoBody, 'thumbData' | 'teaKey' | 'fileEncryptType'>;
1007
+ type ImDiagnosticVoiceBody = Omit<ImVoiceBody, 'teaKey' | 'fileEncryptType'>;
1008
+ type ImDiagnosticFileBody = Omit<ImFileBody, 'teaKey' | 'fileEncryptType'>;
1009
+ type ImDiagnosticStickerBody = Omit<ImStickerBody, 'thumbData'>;
1010
+ /** 诊断可展示的完整正文;密钥、加密策略字节与缩略图二进制在进入 writer 前即被排除。 */
1011
+ interface ImDiagnosticMessageBody {
1012
+ text?: string;
1013
+ system?: ImSystemMessageBody;
1014
+ image?: ImDiagnosticImageBody;
1015
+ video?: ImDiagnosticVideoBody;
1016
+ voice?: ImDiagnosticVoiceBody;
1017
+ file?: ImDiagnosticFileBody;
1018
+ sticker?: ImDiagnosticStickerBody;
1019
+ contact?: ImContactBody;
1020
+ unknown?: UnknownMessageBody;
1021
+ }
1022
+ /**
1023
+ * 产品 Debug 的完整消息投影。
1024
+ *
1025
+ * @remarks 保留排障所需的正文、发送态、回执、mention 与失败信息;SDK 内部排序键、协议 raw、
1026
+ * extensions、媒体 TEA key、文件加密类型和二进制原文永不进入该类型。签名 URL 仍由中央 writer
1027
+ * 在写 sink 前统一替换,避免任何自定义 sink 看见签名材料。
1028
+ */
1029
+ interface ImDiagnosticMessage {
1030
+ id: string;
1031
+ conversationId: string;
1032
+ conversationType: ConversationType;
1033
+ senderId: string;
1034
+ contentType: MessageContentType;
1035
+ body: ImDiagnosticMessageBody;
1036
+ createdAt: number;
1037
+ status: MessageSendStatus;
1038
+ failure?: ImMessageFailure;
1039
+ serverTime?: number;
1040
+ receipt: MessageReceipt;
1041
+ encryption: ImMessageEncryption;
1042
+ mention?: ImMessageMention;
1043
+ refMessageId?: string;
1044
+ edited?: boolean;
1045
+ recalled?: {
1046
+ by: string;
1047
+ at: number;
1048
+ };
1049
+ deletedForEveryone?: {
1050
+ by: string;
1051
+ at: number;
1052
+ reason: 'channel-moderation';
1053
+ };
1054
+ }
1055
+ type ImDiagnosticSink = ImDiagnosticSink$1<ImDiagnosticMessage>;
1056
+ /** SDK 产品 Debug 配置;未传或 `false` 时诊断完全关闭。 */
1057
+ interface ImDebugOptions {
1058
+ enabled: boolean;
1059
+ /** 最低输出级别;缺省为 `trace`。 */
1060
+ level?: ImDiagnosticLevel;
1061
+ /** 允许的分类;缺省为全部稳定分类。 */
1062
+ categories?: readonly ImDiagnosticCategory[];
1063
+ /** 自定义 sink 只能收到中央安全过滤后的 record。 */
1064
+ sink?: ImDiagnosticSink;
1065
+ }
1066
+
1067
+ /**
1068
+ * 媒体对象存储(S3)配置:由宿主注入的**部署基建配置**,SDK 不硬编码 bucket/身份池/host(配置归属宿主层,经 ImClientOptions.media 注入)。
1069
+ * 形状 = im-browser `MediaTransferAdapterConfig`:`identityPoolId` + 独立 `avatar` app-header 位置 + 各 `MediaKind`(image/video/voice/file/sticker)的 `{ bucket, host }` + 可选 `useAccelerateEndpoint`、`maxConcurrentUploads`。
1070
+ * 业务契约对齐 Android AWSUploadManager / CommonConstants.AWS(Cognito 匿名身份池直传、对象 public-read、Transfer Acceleration;见 研究Android图片消息实现.md §A.2)。
1071
+ * ⚠ 各 bucket 需在对象存储侧配置 CORS 允许本站 origin(含分片上传 POST/DELETE + ExposeHeaders ETag);`identityPoolId` 前端可见(对齐 Android 现状)。
1072
+ * 并发字段只由 SDK core 的媒体编排消费;adapter 会忽略该字段。高内存设备可设置 5-6,低内存设备建议 2-3。
1073
+ * ⚠ 后续如需更安全(presigned URL)改 im-browser 的 `MediaTransferPort` 实现即可,本配置与调用方不变。
1074
+ */
1075
+ type ImMediaConfig = MediaTransferAdapterConfig;
1076
+ /**
1077
+ * default 产品面内建 core JSON client 所需的公开部署配置。
1078
+ *
1079
+ * @remarks
1080
+ * core JSON 网关与 `endpointProvider.getCmHttpBaseUrl()` 的 CM Protobuf 服务是两个独立目标,SDK 不会互相
1081
+ * 猜测地址。配置后,联系人、GROUP、CHANNEL 资料/申请/密钥 capability 由 SDK 懒装配;不配置仍可使用
1082
+ * 不依赖 core JSON 的 Direct/CM 能力,但调用相关产品方法会得到稳定 contract 错误。
1083
+ *
1084
+ * @example
1085
+ * ```ts
1086
+ * const coreApi: ImCoreApiOptions = {
1087
+ * baseUrl: 'https://core.example.com',
1088
+ * prefix: '/api/h5',
1089
+ * appVersion: '1.0.0',
1090
+ * envType: 'prod',
1091
+ * }
1092
+ * ```
1093
+ */
1094
+ interface ImCoreApiOptions {
1095
+ /** core JSON 网关基址;配置对象存在时不能为空白。 */
1096
+ baseUrl: string;
1097
+ /** core JSON 默认前缀;缺省使用现有 Web 协议的 `/api/h5`,空串表示直连根路径。 */
1098
+ prefix?: string;
1099
+ /** 写入协议公共参数和版本 header;未提供时沿用协议客户端的空值语义。 */
1100
+ appVersion?: string;
1101
+ /** 签名环境;未提供时使用协议客户端的 `prod` 默认值。 */
1102
+ envType?: string;
1103
+ /** 品牌、商户和渠道标识;部署不需要对应维度时可省略。 */
1104
+ brandId?: string;
1105
+ merchantId?: string;
1106
+ channelId?: string;
1107
+ /** 每次请求读取当前展示语言;协议客户端负责映射为后端短码。 */
1108
+ getLocale?: () => string | null | undefined;
1109
+ }
1110
+ /** Emoji/GIF/贴图外部数据源配置。 */
1111
+ interface ImExpressionOptions {
1112
+ /**
1113
+ * Giphy Web API key;只能由宿主环境配置注入,SDK 不提供内建 key。
1114
+ * 未配置时贴图 Core API 仍可用,调用 Giphy 方法会得到稳定 contract error。
1115
+ */
1116
+ giphy?: Readonly<{
1117
+ apiKey: string;
1118
+ }>;
1119
+ }
1120
+
1121
+ /**
1122
+ * 官方 Chat Kit 和受控诊断工具使用的 SDK internal 入口。
1123
+ * 它暴露完整 client、adapter 装配和领域原子,但仍不属于普通产品 API;生产宿主不得据此解析协议或持有密钥。
1124
+ */
1125
+
1126
+ /**
1127
+ * Chat Kit 可渲染的完整领域消息。
1128
+ *
1129
+ * @remarks 保留正文、mention、回复、编辑、撤回、管理员删除、回执、加密和稳定失败语义;移除 SDK 内部 `sortKey`、原始协议 `raw`、
1130
+ * 包含 history cursor 的 `extensions` 以及媒体 key。Chat Kit 不得自行恢复或推断这些内部字段。
1131
+ */
1132
+ type AdvancedMessage = ImMessageView;
1133
+
1134
+ /**
1135
+ * Chat Kit 与宿主应用之间的接入合同。
1136
+ *
1137
+ * PC Web、H5 等宿主通过本文件把“当前登录的是谁、IM 服务地址是什么、使用哪种语言、
1138
+ * 如何访问业务 HTTP 接口、鉴权失效后怎么回登录页”等平台能力交给 Chat Kit。Chat Kit
1139
+ * 只使用这些明确提供的能力,不直接读取宿主的登录 Store、运行时配置、路由或全局对象。
1140
+ *
1141
+ * 接入顺序通常是:宿主在应用初始化时构造 `ImHostEnvironment`,用其中稳定的函数配置
1142
+ * `ChatRuntime`;登录结果变化后,再由 Session Store 把最新账号应用到 Runtime。
1143
+ * token 只允许在调用参数和非响应式闭包中流转,禁止写入 Pinia、SSR 数据和诊断日志。
1144
+ */
1145
+
1146
+ /** 宿主登录成功后交给 Chat Kit 的最小账号信息;`null` 表示当前没有登录用户。 */
1147
+ interface ImHostSession {
1148
+ /** 当前用户 ID,用于隔离该账号的连接、本机消息库和页面缓存。 */
1149
+ userId: string;
1150
+ /** 连接 IM 服务所需的短期凭据;属于敏感信息,不能展示、持久化或记录日志。 */
1151
+ token: string;
1152
+ }
1153
+ /**
1154
+ * 宿主应用必须提供的 IM 运行环境。
1155
+ *
1156
+ * `session` 和 `locale` 是会变化的响应式值:账号变化由宿主接入代码驱动 Runtime 重置,
1157
+ * 语言变化会让消息文案重新渲染。其余 getter 可能在连接、上传或资料请求的异步阶段调用,
1158
+ * 因此宿主应在 setup 阶段先取得配置并保存在闭包中,不要在 getter 内临时调用只能在
1159
+ * Nuxt/Vue setup 上下文中使用的 API。
1160
+ */
1161
+ interface ImHostEnvironment {
1162
+ /** 当前宿主账号;登录、退出或切账号时发生变化,由接入层据此应用或清空 IM 账号。 */
1163
+ session: Readonly<Ref<ImHostSession | null>>;
1164
+ /**
1165
+ * 在建链、重连和前台恢复时读取宿主当前权威登录态。
1166
+ *
1167
+ * 跨标签页或其它应用代码可能直接清理持久化凭据而没有先更新 `session` Ref;提供此 getter 后,
1168
+ * SDK 可以在下一次连接动作前停止使用旧 token。未提供时回退读取 `session.value`。
1169
+ */
1170
+ getSession?: () => ImHostSession | null;
1171
+ /** 清除宿主自己的登录态;Chat Kit 完成 IM 退出后,由页面按产品流程调用。 */
1172
+ clearSession: () => void;
1173
+ /** 返回当前浏览器实例的稳定设备标识,避免同一账号的多个页面实例被服务端误判为同一连接。 */
1174
+ getDeviceToken: () => string;
1175
+ /** 返回实时消息 WebSocket 地址和 IM 业务 HTTP 地址;缺失或非法时连接会明确失败。 */
1176
+ getEndpoints: () => {
1177
+ wsUrl: string;
1178
+ cmHttpBaseUrl: string;
1179
+ };
1180
+ /** 返回图片、视频、语音和文件上传下载配置;返回 `undefined` 时媒体收发能力不可用。 */
1181
+ getMediaConfig: () => ImMediaConfig | undefined;
1182
+ /** 返回 GIF provider 配置;密钥只能来自宿主环境配置,未配置时 Giphy 请求明确不可用。 */
1183
+ getExpressionOptions?: () => ImExpressionOptions | undefined;
1184
+ /** 返回 IM 诊断开关;生产环境必须默认关闭,且任何模式都不能记录 token、密钥或原始二进制。 */
1185
+ getDebugOptions?: () => boolean | ImDebugOptions;
1186
+ /** 返回宿主已有的业务 HTTP Client,供联系人、群资料、成员资料和加密密钥请求复用。 */
1187
+ getCoreClient: () => ApiProtocolClient | undefined;
1188
+ /** 返回 core JSON 部署配置;没有宿主 HTTP Client 时由 SDK 内部创建协议 client。 */
1189
+ getCoreApi?: () => ImCoreApiOptions | undefined;
1190
+ /** 后端识别客户端与加密密钥范围所需的类型值;PC Web 和 H5 当前使用 `2`。 */
1191
+ clientType: number;
1192
+ /** IM 鉴权过期、被踢或被封禁时通知宿主;宿主据此跳转登录页或展示产品弹窗。 */
1193
+ onAuthInvalid: (issue: ImAuthIssue) => void;
1194
+ /** 当前界面语言;消息摘要、系统消息和失败提示会随它变化而重新生成。 */
1195
+ locale: Readonly<Ref<ImLocale>>;
1196
+ }
1197
+
1198
+ /**
1199
+ * 把 SDK 中与语言无关的消息数据转换成当前页面语言的可见文本。
1200
+ *
1201
+ * 消息气泡、会话列表摘要和失败提示都通过这里生成,组件不需要理解系统消息事件码或失败码。
1202
+ * 本模块不保存语言状态,只在每次格式化时读取宿主传入的 `locale()`,因此切换语言后同一条消息
1203
+ * 可以直接重新渲染。历史缓存若还没有结构化文案字段,会暂时显示旧摘要或后端错误原文。
1204
+ */
1205
+
1206
+ /** 生成 IM 文案所需的三项页面信息;显式传入后即可独立于任意 Store 使用。 */
1207
+ interface ImTextContext {
1208
+ /** 返回当前界面语言;函数在组件渲染期间被调用,语言变化会触发 Vue 重新渲染。 */
1209
+ locale: () => ImLocale;
1210
+ /** 把用户 ID 转为此处应展示的名称,例如优先备注、群昵称,再回退公开昵称。 */
1211
+ resolveName: (uid: string) => string;
1212
+ /** 返回当前用户 ID,用于把系统消息中的本人显示为“你”。 */
1213
+ selfId: () => string;
1214
+ }
1215
+ /**
1216
+ * 创建消息组件可直接调用的四个文案函数。
1217
+ *
1218
+ * `systemMessage` 生成入群、退群、改名等系统消息;`mediaPlaceholder` 生成“图片”“语音 00:08”
1219
+ * 之类占位;`lastMessage` 生成会话列表最后一条摘要;`failureText` 生成发送失败原因。
1220
+ * 每次调用都会读取最新语言和名称,不会为长消息列表中的每一条消息创建额外的 `computed`。
1221
+ */
1222
+ declare function useImText(ctx: ImTextContext): {
1223
+ systemMessage: (message: AdvancedMessage) => string;
1224
+ mediaPlaceholder: (message: AdvancedMessage) => string;
1225
+ lastMessage: (session: ImSession) => string;
1226
+ failureText: (message: AdvancedMessage) => string;
1227
+ };
1228
+
1229
+ /** 根据 Core body kind 选择宿主 renderer 的无样式正文组件。 */
1230
+ declare const ImThreadBodySwitch: any;
1231
+
1232
+ /** 按 Core timeline 顺序渲染 slots 与正文 registry 的无样式列表组件。 */
1233
+ declare const ImThreadItems: any;
1234
+
1235
+ /** Surface viewport 首批可调参数。 */
1236
+ interface ImThreadViewportOptions {
1237
+ followThreshold: number;
1238
+ olderPrefetchThreshold: number;
1239
+ visibleReadDelay: number;
1240
+ visibleMargin: number;
1241
+ /** 顶部日期浮层相对消息滚动根的像素位置。 */
1242
+ activeDateOffset: number;
1243
+ /** 用户停止滚动后日期浮层保持可见的毫秒数。 */
1244
+ activeDateIdleDelay: number;
1245
+ }
1246
+ /** 宿主日期浮层 slot 消费的稳定状态;视觉和文案样式仍由 PC/H5 决定。 */
1247
+ interface ImThreadActiveDateSlotState {
1248
+ label: string;
1249
+ visible: boolean;
1250
+ }
1251
+ /** 新会话首次渲染时的有界定位与短窗补页参数。 */
1252
+ interface ImThreadInitialViewportOptions {
1253
+ /** false 时由宿主自行决定何时定位;可见已读仍使用 Surface 的统一范围。 */
1254
+ enabled?: boolean;
1255
+ /** 内容不足一屏时最多主动加载的 older 页数;默认 8。 */
1256
+ maxFillPages?: number;
1257
+ }
1258
+ /** 结构 slots 可调用的稳定 Surface actions。 */
1259
+ interface ImThreadSurfaceSlotActions {
1260
+ /** 返回 Surface 唯一滚动根,供宿主媒体观察等平台能力复用;宿主不得建立第二套滚动控制器。 */
1261
+ getViewportElement: () => HTMLElement | null;
1262
+ loadOlder: () => Promise<number>;
1263
+ retryOlder: () => Promise<number>;
1264
+ loadNewer: () => Promise<number>;
1265
+ retryNewer: () => Promise<number>;
1266
+ jumpToLatest: () => Promise<boolean>;
1267
+ /** 在当前已加载窗口内定位消息;找不到或布局不可测时返回 false。 */
1268
+ scrollToMessage: (messageId: string, options?: ScrollIntoViewOptions) => boolean;
1269
+ /** 必要时先替换为目标附近窗口,再等待真实消息节点登记并完成定位。 */
1270
+ locateMessage: (messageId: string, options?: ScrollIntoViewOptions) => Promise<boolean>;
1271
+ /** 把焦点还给当前 Surface 的唯一消息滚动根,不查询宿主全局 DOM。 */
1272
+ focusViewport: () => void;
1273
+ }
1274
+ /** 组合 Thread Core、无样式 viewport 和结构 slots 的 Managed Headless Surface。 */
1275
+ declare const ImThreadSurface: any;
1276
+
1277
+ /** Viewport 每次滚动结算后上报的统一可见范围。 */
1278
+ interface ImThreadViewportSettledSnapshot {
1279
+ visibleMessageIds: readonly string[];
1280
+ newestVisibleMessageId?: string;
1281
+ isAtBottom: boolean;
1282
+ newMessageCount: number;
1283
+ }
1284
+ /** Surface initial controller 可调用的无产品语义滚动能力。 */
1285
+ interface ImThreadViewportExposed {
1286
+ /** 返回 Surface 自己持有的唯一滚动根,供宿主媒体观察等平台能力复用;调用方不得修改其所有权。 */
1287
+ getViewportElement: () => HTMLElement | null;
1288
+ /** 清空当前视口维护的未见消息集合。 */
1289
+ clearNewMessageBadge: () => void;
1290
+ /** 日期浮层退场后恢复被碰撞日期节点;不改变消息滚动位置。 */
1291
+ clearActiveDate: () => void;
1292
+ /** 当前已加载窗口是否位于底部阈值内。 */
1293
+ isAtBottom: Readonly<{
1294
+ value: boolean;
1295
+ }>;
1296
+ /** 消息滚动根是否已有非零布局尺寸。 */
1297
+ isMeasurable: () => boolean;
1298
+ /** 当前已加载内容是否超过滚动根可视高度。 */
1299
+ isScrollable: () => boolean;
1300
+ /** 当前未见消息数量。 */
1301
+ newMessageCount: Readonly<{
1302
+ value: number;
1303
+ }>;
1304
+ /**
1305
+ * 标记下一次尾部追加来自历史 newer 分页。
1306
+ *
1307
+ * Surface 在指定消息定位后主动扩展短窗口时,需要在调用 loadNewer 前设置该标记,
1308
+ * 避免尾部新增节点被当作实时消息并触发吸底或未见消息计数。
1309
+ */
1310
+ prepareHistoryPageAppend: () => void;
1311
+ /** 主动 newer 分页失败或没有新增消息时,释放历史追加标记。 */
1312
+ cancelHistoryPageAppend: () => void;
1313
+ /** 接收未进入当前连续消息窗口的实时对端消息 id。 */
1314
+ trackRemoteMessageIds: (messageIds: readonly string[], input: {
1315
+ isAtBottom: boolean;
1316
+ isHistoryWindow: boolean;
1317
+ isPageVisible: boolean;
1318
+ }) => boolean;
1319
+ /** 只滚动消息根到当前已加载窗口的最新端。 */
1320
+ scrollToLatest: (behavior?: ScrollBehavior) => void;
1321
+ /** 在当前消息根内定位已登记节点;目标不存在或布局不可测时返回 false。 */
1322
+ scrollToMessage: (messageId: string, options?: ScrollIntoViewOptions) => boolean;
1323
+ /** 菜单或弹层关闭后把键盘焦点还给唯一消息滚动根,不改变滚动位置。 */
1324
+ focusViewport: () => void;
1325
+ }
1326
+ /** 无产品样式的唯一消息滚动容器。 */
1327
+ declare const ImThreadViewport: any;
1328
+
1329
+ /** Surface renderer registry 可接收的稳定消息正文种类。 */
1330
+ type ImMessageBodyKind = MessageContentType | 'gif' | 'decrypt-failed';
1331
+
1332
+ /** 一个稳定正文 kind 对应的宿主 Vue renderer 注册项。 */
1333
+ interface ImMessageRendererRegistration<TKind extends ImMessageBodyKind = ImMessageBodyKind> {
1334
+ /** Core `resolveMessageBodyKind` 返回的稳定正文种类。 */
1335
+ kind: TKind;
1336
+ /** 宿主提供的 Vue 组件;Surface 只传标准消息与 presentation。 */
1337
+ component: Component;
1338
+ }
1339
+ /** 创建后只读的正文 renderer registry;不保存消息数据或 Pinia 状态。 */
1340
+ interface ImMessageRendererRegistry {
1341
+ /** 返回精确 kind 的 renderer;未注册时返回 undefined,由 BodySwitch 显式降级。 */
1342
+ resolve: (kind: ImMessageBodyKind) => Component | undefined;
1343
+ /** 返回创建时冻结的注册 kind,供诊断和 consumer contract 使用。 */
1344
+ kinds: readonly ImMessageBodyKind[];
1345
+ }
1346
+ /**
1347
+ * 创建正文 renderer registry。
1348
+ *
1349
+ * 同 kind 重复注册会立即抛错,避免不同宿主装配顺序静默覆盖组件。registry 不支持运行时热替换。
1350
+ */
1351
+ declare function createMessageRendererRegistry(registrations: readonly ImMessageRendererRegistration[]): ImMessageRendererRegistry;
1352
+
1353
+ /** 会话 sibling 可插入 timeline 的无产品样式装饰项。 */
1354
+ interface ImThreadEncryptionItem {
1355
+ kind: 'encryption';
1356
+ /** Vue 列表和 prepend 对比使用的稳定 key。 */
1357
+ key: string;
1358
+ }
1359
+ /** 会话 sibling 显式注入的 timeline 装饰项;Core 不根据会话类型自行猜测。 */
1360
+ type ImThreadDecoration = ImThreadEncryptionItem;
1361
+ /** 会话 sibling 对可见消息已读目标的显式解析器;null 表示本轮暂缓。 */
1362
+ type ImThreadReadTargetResolver = (messageId: string) => string | null;
1363
+ /** 会话 sibling 向共享 Surface 提供的已读动作合同。 */
1364
+ interface ImThreadReadActions {
1365
+ /** 把已确认可见的消息交给所属会话已读策略。 */
1366
+ markRead: (messageId: string) => unknown | Promise<unknown>;
1367
+ /** Direct 媒体真正开始播放后可提供单条消费回执。 */
1368
+ markMediaConsumed?: (messageId: string) => unknown | Promise<unknown>;
1369
+ /** CHANNEL 等需要处理本地乐观时序的 sibling 可覆盖已读目标。 */
1370
+ resolveTarget?: ImThreadReadTargetResolver;
1371
+ }
1372
+ /** Managed Headless Surface 首批会向 typed hooks 暴露的用户意图。 */
1373
+ type ImThreadIntent = Readonly<{
1374
+ kind: 'load-older';
1375
+ }> | Readonly<{
1376
+ kind: 'retry-older';
1377
+ }> | Readonly<{
1378
+ kind: 'load-newer';
1379
+ }> | Readonly<{
1380
+ kind: 'jump-to-latest';
1381
+ }> | Readonly<{
1382
+ kind: 'locate-message';
1383
+ messageId: string;
1384
+ }> | Readonly<{
1385
+ kind: 'retry-message';
1386
+ messageId: string;
1387
+ }> | Readonly<{
1388
+ kind: 'visible-range';
1389
+ messageIds: readonly string[];
1390
+ }>;
1391
+
1392
+ /** 正文 renderer 按需读取 sibling read actions;未装配时返回 undefined。 */
1393
+ declare function useOptionalImThreadReadActions(): ImThreadReadActions | undefined;
1394
+
1395
+ /** Surface 每次渲染直接读取的 Thread Store 投影;context 不保存第二份 messages。 */
1396
+ interface ImThreadSurfaceState {
1397
+ /** 当前 Thread Store 已成功打开的精确会话;null 表示尚未打开。 */
1398
+ activeConversation: ConversationRef<ProductConversationListType> | null;
1399
+ /** 当前 handle 的可见消息数组引用。 */
1400
+ messages: readonly ImMessageView[];
1401
+ /** 打开会话时冻结的首条未读入口。 */
1402
+ entryUnread: ConversationEntryUnreadSnapshot;
1403
+ /** handle 首次快照是否已经可用。 */
1404
+ ready: boolean;
1405
+ /** 当前是否正在执行窗口操作。 */
1406
+ loading: boolean;
1407
+ /** 更早历史分页是否正在执行;未提供时由 Surface 使用 loading 兼容旧宿主。 */
1408
+ loadingOlder?: boolean;
1409
+ /** 更新历史分页是否正在执行;未提供时由 Surface 使用 loading 兼容旧宿主。 */
1410
+ loadingNewer?: boolean;
1411
+ /** 更早历史分页的显式错误;省略时使用统一 error 兼容旧宿主。 */
1412
+ olderError?: unknown | null;
1413
+ /** 更新历史分页的显式错误;省略时使用统一 error 兼容旧宿主。 */
1414
+ newerError?: unknown | null;
1415
+ /** 当前稳定错误投影;null 表示无错误。 */
1416
+ error: unknown | null;
1417
+ /** SDK 连续窗口是否仍有更早历史。 */
1418
+ hasOlder: boolean;
1419
+ /** SDK 连续窗口是否仍有更新历史。 */
1420
+ hasNewer: boolean;
1421
+ /** 当前是否位于 around/搜索产生的历史窗口。 */
1422
+ isHistoryWindow: boolean;
1423
+ /** 当前活动会话待 Surface 消费的实时对端消息版本。 */
1424
+ activeRemoteMessageRevision?: number;
1425
+ /** 宿主页面是否可见;省略时读取 context.isPageVisible。 */
1426
+ pageVisible?: boolean;
1427
+ /** 宿主页面是否可见且可交互;省略时读取 context.isPageEngaged。 */
1428
+ pageEngaged?: boolean;
1429
+ }
1430
+ /** Surface 可以调用的共享 Thread actions;网络、窗口和 Outbox 真相仍由上游 Store/SDK 持有。 */
1431
+ interface ImThreadSurfaceActions {
1432
+ /** 请求一页更早历史;返回实际新增消息数。 */
1433
+ loadOlder: (options?: {
1434
+ force?: boolean;
1435
+ }) => Promise<number>;
1436
+ /** 手动重试更早历史;省略时 Surface 用 `loadOlder({ force:true })`。 */
1437
+ retryOlder?: () => Promise<number>;
1438
+ /** 在历史窗口底部请求一页更新历史;返回实际新增消息数。 */
1439
+ loadNewer?: (options?: {
1440
+ force?: boolean;
1441
+ }) => Promise<number>;
1442
+ /** 手动重试更新历史;省略时 Surface 用 `loadNewer({ force:true })`。 */
1443
+ retryNewer?: () => Promise<number>;
1444
+ /** 把 Thread Store 返回 latest;返回 false 表示失败或旧请求已失效。 */
1445
+ jumpToLatest?: () => Promise<boolean>;
1446
+ /** 用目标消息附近的连续窗口取代当前窗口;DOM 定位仍由 Surface 统一完成。 */
1447
+ loadAround?: (messageId: string) => Promise<boolean>;
1448
+ /** 原子取走尚未进入当前连续窗口的实时对端消息 id。 */
1449
+ drainActiveRemoteMessageIds?: () => readonly string[];
1450
+ /** 接收统一可见消息范围;不得在实现里重新扫描 DOM。 */
1451
+ onVisibleRange?: (messageIds: readonly string[]) => void;
1452
+ }
1453
+ /** 显式 action hooks;只能观察结果,不能改写共享 action 或发起隐藏重试。 */
1454
+ interface ImThreadSurfaceHooks {
1455
+ beforeAction?: (intent: ImThreadIntent) => void;
1456
+ afterAction?: (intent: ImThreadIntent, result: unknown) => void;
1457
+ onError?: (intent: ImThreadIntent, error: unknown) => void;
1458
+ }
1459
+ /**
1460
+ * Host 把当前 Pinia refs/actions 适配为 Surface context。
1461
+ *
1462
+ * `getState` 每次返回上游当前引用;context 不复制 messages、draft、history 或权限状态。
1463
+ */
1464
+ interface ImThreadSurfaceContext {
1465
+ getState: () => ImThreadSurfaceState;
1466
+ actions: ImThreadSurfaceActions;
1467
+ formatDate: (timestamp: number) => string;
1468
+ leadingItems?: () => readonly ImThreadDecoration[];
1469
+ beforeMessageItems?: (message: ImMessageView) => readonly ImThreadDecoration[];
1470
+ /** 新消息追加时是否代表当前账号主动发送;用于离底但仍应跟随的文本发送。 */
1471
+ shouldFollowMessage?: (message: ImMessageView) => boolean;
1472
+ /** 页面是否可见;用于跟随和 badge,不读取 document。 */
1473
+ isPageVisible?: () => boolean;
1474
+ /** 页面是否同时可见且可交互;用于已读,不读取 document.hasFocus。 */
1475
+ isPageEngaged?: () => boolean;
1476
+ }
1477
+ /** 创建不拥有状态的 Surface context。 */
1478
+ declare function createImThreadSurfaceContext(context: ImThreadSurfaceContext): ImThreadSurfaceContext;
1479
+ /** 在组件树中提供当前 Surface context;不会创建 Pinia 或 Runtime。 */
1480
+ declare function provideImThreadSurfaceContext(context: ImThreadSurfaceContext): void;
1481
+ /** 读取已提供的 Surface context;缺失时 fail-fast,避免静默创建第二套状态。 */
1482
+ declare function useImThreadSurfaceContext(): ImThreadSurfaceContext;
1483
+
1484
+ interface ThreadVisibilityTargetsOptions<TItem> {
1485
+ /** 返回 Managed Surface 的唯一滚动根;控制器只观察,不改变滚动位置。 */
1486
+ getRoot: () => HTMLElement | null;
1487
+ /** 返回目标在当前账号和会话内的稳定复合身份。 */
1488
+ getKey: (item: TItem) => string;
1489
+ /** 节点首次进入视口时立即触发,适合创建缩略图或 poster。 */
1490
+ onRevealed?: (item: TItem) => void;
1491
+ /** 轻量资源在进入视口前多少像素开始准备;不会扩大稳定可见或高清缓存读取范围。 */
1492
+ revealMarginPx?: number;
1493
+ /** 用户开始或停止滚动时触发;宿主可据此暂停高频占位动画。 */
1494
+ onScrollingChange?: (scrolling: boolean) => void;
1495
+ /** 节点在滚动停止后仍可见时首次触发,适合安排 cache-only 读取。 */
1496
+ onActivated?: (item: TItem) => void;
1497
+ /** 每次稳定可见集合变化时触发,顺序按节点距离滚动根中心由近到远。 */
1498
+ onSettledVisibleChange?: (items: readonly TItem[]) => void;
1499
+ /** 激活和稳定集合要求的最小可见比例;默认只要有交集即可。 */
1500
+ minimumIntersectionRatio?: number;
1501
+ /** 最多保留多少个稳定可见目标;默认不限制。 */
1502
+ maxSettledVisible?: number;
1503
+ scrollIdleMs?: number;
1504
+ visibilityDwellMs?: number;
1505
+ }
1506
+ interface ThreadVisibilityTargets<TItem> {
1507
+ register: (item: TItem, element: HTMLElement | null) => void;
1508
+ isRevealed: (item: TItem) => boolean;
1509
+ isActivated: (item: TItem) => boolean;
1510
+ isSettledVisible: (item: TItem) => boolean;
1511
+ isScrolling: () => boolean;
1512
+ /** 宿主已掌握滚动事件时可主动通知;控制器仍会监听自身滚动根。 */
1513
+ notifyScroll: () => void;
1514
+ refresh: () => void;
1515
+ reset: () => void;
1516
+ dispose: () => void;
1517
+ }
1518
+ type ThreadVisibilityTargetsFactory<TItem> = (options: ThreadVisibilityTargetsOptions<TItem>) => ThreadVisibilityTargets<TItem>;
1519
+ /**
1520
+ * 管理 Thread Surface 子节点的两阶段可见性。
1521
+ *
1522
+ * 首次进入视口立即揭示轻量资源;滚动停止并持续可见后才激活较重工作。IntersectionObserver
1523
+ * 不可用时仍按滚动根和节点真实几何计算,不会把整个消息窗口误判为可见。
1524
+ */
1525
+ declare function createThreadVisibilityTargets<TItem>(options: ThreadVisibilityTargetsOptions<TItem>): ThreadVisibilityTargets<TItem>;
1526
+
1527
+ interface ThreadVisibilityTargetGroupOptions {
1528
+ /** 返回 Managed Surface 的唯一滚动根。 */
1529
+ getRoot: () => HTMLElement | null;
1530
+ /** 所有通道共享的轻量资源预热距离。 */
1531
+ revealMarginPx?: number;
1532
+ /** 所有通道共享的稳定激活最小可见比例。 */
1533
+ minimumIntersectionRatio?: number;
1534
+ scrollIdleMs?: number;
1535
+ visibilityDwellMs?: number;
1536
+ }
1537
+ interface ThreadVisibilityTargetGroup<TItem> {
1538
+ /**
1539
+ * 为图片、视频等资源通道创建兼容现有 Host 的 factory。
1540
+ *
1541
+ * 同一通道只能创建一次;每个通道仍保留自己的 key 和回调,但底层共享一个 scroll listener
1542
+ * 与一组 IntersectionObserver,避免同一消息滚动根重复做可见性调度。
1543
+ */
1544
+ createFactory: (channel: string) => ThreadVisibilityTargetsFactory<TItem>;
1545
+ refresh: () => void;
1546
+ dispose: () => void;
1547
+ }
1548
+ /**
1549
+ * 让同一滚动根上的多类媒体共享可见性 controller,同时向既有 Host 暴露独立端口。
1550
+ *
1551
+ * 资源的 reveal/activate 回调仍按通道分发;`maxSettledVisible` 也在通道内独立截断,
1552
+ * 因而视频目标不会占用图片高清缓存的稳定可见名额。
1553
+ */
1554
+ declare function createThreadVisibilityTargetGroup<TItem>(groupOptions: ThreadVisibilityTargetGroupOptions): ThreadVisibilityTargetGroup<TItem>;
1555
+
1556
+ export { ImThreadBodySwitch, ImThreadItems, ImThreadSurface, ImThreadViewport, createImThreadSurfaceContext, createMessageRendererRegistry, createThreadVisibilityTargetGroup, createThreadVisibilityTargets, provideImThreadSurfaceContext, useImText, useImThreadSurfaceContext, useOptionalImThreadReadActions };
1557
+ export type { ImHostEnvironment, ImHostSession, ImMessageRendererRegistration, ImMessageRendererRegistry, ImTextContext, ImThreadActiveDateSlotState, ImThreadInitialViewportOptions, ImThreadSurfaceActions, ImThreadSurfaceContext, ImThreadSurfaceHooks, ImThreadSurfaceSlotActions, ImThreadSurfaceState, ImThreadViewportExposed, ImThreadViewportOptions, ImThreadViewportSettledSnapshot, ThreadVisibilityTargetGroup, ThreadVisibilityTargetGroupOptions, ThreadVisibilityTargets, ThreadVisibilityTargetsFactory, ThreadVisibilityTargetsOptions };