通用消息列表设计方案 · 2026-11-05

让实时字幕、服务端历史和图片消息,都进入同一条可靠的消息列表。

本文面向“与 AI 对话的消息列表”,先讲可复用的消息身份、内容、同步、分页和滚动机制,再列出字幕、图片和流式回复等可选适配,最后标明当前示例的具体配置。

适用:AI 对话、语音转写、流式回复、图片和文件消息。本文结合公开 AI 消息产品资料整理。整理。

阅读方式:通用核心解决所有消息列表都要面对的身份、顺序、窗口和交互;可选能力按产品需要接入;“本次落地配置”只描述当前示例的选择,不代表所有产品都应照搬。

01 / 要解决的问题

消息列表的难点在于消息来自多个来源、内容持续变化、历史需要分页、用户会在加载和滚动之间切换。

实时字幕先到

每次收到一小段增量,页面要及时更新,但不能每段都创建新气泡。

历史消息后到

服务端历史是正式来源,需要把本地字幕确认成同一条正式消息。

往上翻页

只读取上一页,不能重新整理全部记录,也不能让当前阅读位置跳走。

文字和图片占位

发送未确认时先显示占位,历史回来后按请求 ID更新原行。

切换和冷启动

切 Tab 保留窗口;重启只恢复正式消息,临时消息不伪装成历史。

长会话变慢

列表行很多、卡片很高、流式内容变化时,滚动不能被测量反复打断。

一句话方案:把消息身份、内容、来源和列表位置分开定义,所有展示行使用同一消息模型,读取时只查询当前窗口和必要关联数据。

公开 AI 消息产品中哪些功能会影响列表设计

已查证能力对通用列表的要求依据
消息区混合文本、附件、工具活动、问题和审批卡片。消息应包含类型化内容块,不只是一段字符串;操作失败在卡片内呈现。官方消息说明
手机切走后按会话保留草稿。草稿归会话,不随可见行销毁。官方移动端说明
支持消息搜索并回到对应位置。目标 ID 驱动查询、挂载、测量和定位;未加载目标需要数据层查找能力。官方搜索说明
手机与桌面共享会话,隐藏 Bot 不删除历史。目录可见状态与消息数据分离,切设备后按服务端正式历史恢复。官方排障说明

上述是产品行为依据,不代表当前示例已实现工具审批、搜索、草稿或多设备实时同步;消息列表只预留可承载这些内容的边界。

02 / 总体设计:通用核心、可选适配和当前示例配置

消息列表由三层组成:统一消息模型是通用核心;字幕、发送占位、媒体和搜索是可选适配;数据库表名、页大小和重启策略属于当前示例配置。展示层只面对统一消息对象,辅助关系只负责帮助它更新。

主链路:用户操作 → 请求和处理 → 服务端数据 → 本地保存 → UI 快照 1. UI 与列表状态滚动、重试、定位发送、复制、媒体操作保存阅读锚点 2. 消息处理层身份匹配、合并、事务窗口查询、失效检查发布一致快照 3. 服务端接口正式消息和版本来源关系和游标最新页 / 更早页 / 目标页 4. 本地数据库统一消息正文字幕关系和同步进度索引与本地版本 用户操作 请求 消息 + 游标 事务写 / 查询 窗口数据 快照更新 可选发送 / 上传请求 ID、资源 ID、确认结果 返回结果 阅读位置和生命周期列表状态保存跟随最新、阅读历史、定位请求和锚点;切 Tab、后台、登出或新会话会让旧请求失效。 实线:主链路 虚线:可选发送扩展 蓝绿色箭头:数据返回或数据库结果

通用核心、可选能力和当前示例配置

层次必须解决什么本文示例
通用核心稳定消息身份、内容块、服务端版本、会话内顺序、分页游标、窗口快照、错误和滚动锚点。所有 AI 消息列表都可复用。
可选适配字幕追加/替换、发送占位、图片上传、工具卡片、搜索定位、编辑删除和多端同步。有该能力才增加对应来源关系和动作入口。
当前示例配置五张同名新表;每页 50 条加 1 条探测;当前进程 local;重启只展示 server;进入页面刷新最新历史。这是 当前产品 当前取舍,需按产品要求重新确认。

责任边界

责任负责者关键结果
消息和分页数据会话数据层 / 原生宿主保存消息、发请求、更新游标、拒绝旧会话结果。
阅读状态列表状态层保存跟随最新、阅读历史、锚点、定位请求和加载状态。
手势和偏移执行原生 ScrollView / 倒置列表执行拖动、惯性、定位命令,反馈当前偏移和测量结果。
内容操作消息卡片和业务能力复制、预览、播放、停止、重试等动作按 ui_id + block_id 处理。

03 / 完整链路:来源、请求、数据库和 UI 如何交接

1. 收到来源字幕增量、正式历史或发送确认进入客户端。
2. 识别身份用 event_id、message_id、version、client_request_id建立对应关系。
3. 更新数据库同一事务写展示消息和字幕来源关系。
4. 形成快照按服务端顺序、本地顺序和窗口生成 UI 数据。
5. 渲染交互只挂载附近行,滚动时按游标预取上一页。

字幕到 UI

字幕事件按会话、角色和时间间隔归入本地分组,delta 拼成一条 local 消息,UI 看到这条消息继续增长。每个事件保存 event_id,之后和正式历史按事件对应。

历史到 UI

进入聊天或重新进入页面请求最新历史;正式消息按 message_id、version、timeline_index 保存。UI 从统一展示消息表读取,不能另存第二份展示正文。

历史确认字幕

历史消息带 live_source.provider_event_ids 时查找对应本地分组。完整一对一确认时,原 local 行改为 server,保留 ui_id;未覆盖的剩余字幕形成新的 local 行。

图片和文字占位

发送前生成 client_request_id,先写 local 消息;图片上传得到 asset_id或URL后写入 blocks。历史带相同 client_request_id时更新原 ui_id;图片 URL 只是内容地址。

上翻历史的完整闭环

用户接近顶部列表状态判断本地窗口不足且 has_more=true。
发起更早请求带 conversation_id 和 before_cursor;同一会话只保留一个进行中请求。
服务端返回返回消息、来源、版本和新的 before_cursor。
事务提交去重、保存消息、更新游标;成功后才通知 UI。
窗口扩展插入前页,按稳定 ui_id 保持当前阅读锚点。

最新刷新走同一条闭环但不带 before_cursor;搜索定位会带目标消息 ID或服务端查找条件;发送和上传属于可选扩展,返回结果仍由消息处理层按请求 ID确认。

部分确认的具体例子

本地字幕是 A+B,服务端历史只声明覆盖 A。事务结果是:正式消息 A 使用服务端身份;未确认的 B 继续保留为 local;A 不再复制成第二条。之后 B 被另一条正式消息确认时,再按同一规则原地更新对应行。

具体 case:从“今天”到正式消息

e1:今天保存事件;创建 ui-001 / local。
e2:天气不错更新同一行 blocks;UI 只刷新这一气泡。
历史 m-701声明覆盖 e1、e2;检查 ID、版本及来源。
原行确认同一事务将 ui-001 更新为 server,并记录覆盖关系。
重启只读正式 ui-001;旧 local 不自动恢复。

写入如何驱动 UI

由会话消息处理对象串行处理事件,校验通过后提交数据库事务;提交成功再提升本地变化版本并通知 UI。UI 获取一致的窗口快照,按 ui_id 复用未变对象。这是“数据变化驱动界面”,不要求数据库必须自动监听。当前实现使用通知加窗口快照;未来可改为按 ID 的增量通知,但不必额外持久化一份变更正文。

若写入失败,不推进游标、不标记来源已覆盖、不发成功通知;同一批重试必须可重复执行。较旧 server_version 不能覆盖新版本;同版本内容矛盾应报校验错误,不能静默覆盖。

04 / 统一消息模型:内容只有一份,状态随来源变化

关键规则:origin=server 表示已经正式确认,且对应服务端字段必须齐全;不要用文本、相近时间、图片 URL或自增序号猜身份。

同一模型的字段变化

第一行给出空内容模型;后续只列发生变化的字段。落库是保存动作,不是另外一套消息模型。

阶段JSON:首次模型 / 后续变化含义
0. 创建模型
{
  "ui_id": "ui-001",
  "conversation_id": "c-001",
  "role": "assistant",
  "origin": "local",
  "blocks": [],
  "local_kind": "live_transcript",
  "group_id": "g-001",
  "local_sequence": 1,
  "server_message_id": null,
  "server_version": null,
  "server_timeline_index": null,
  "client_request_id": null,
  "content_state": "generating",
  "updated_revision": 0
}
创建于本次进程,不伪造正式消息身份。
1. e1 到达并提交
{
  "blocks": [{ "block_id": "t1", "type": "text", "text": "今天" }],
  "updated_revision": 1
}
事件 e1、合并后的展示行和关系一起提交;成功后通知 UI。
2. e2 到达并提交
{
  "blocks": [{ "block_id": "t1", "type": "text", "text": "今天天气不错" }],
  "updated_revision": 2
}
事件 e2 与合并结果再次一起提交,仍是同一 ui_id。
3. 字幕结束并提交
{
  "content_state": "ended",
  "updated_revision": 3
}
结束只表示不再增长,origin 仍为 local;当前示例每一步都可在提交成功后通知 UI。
4. 历史完整确认并提交
{
  "origin": "server",
  "server_message_id": "m-701",
  "server_version": 1,
  "server_timeline_index": 701,
  "local_kind": null,
  "group_id": null,
  "local_sequence": null,
  "content_state": "completed",
  "updated_revision": 4
}
ui_id=ui-001 不变;原字幕关系留在辅助表中。

服务端、数据库、UI 的形状怎样对应

{
  "message_id": "m-701",
  "conversation_id": "c-001",
  "version": 1,
  "timeline_index": 701,
  "role": "assistant",
  "blocks": [
    {
      "block_id": "t1",
      "type": "text",
      "text": "今天天气不错"
    }
  ],
  "live_source": {
    "live_session_ref": "live-01",
    "provider_event_ids": [
      "e1",
      "e2"
    ]
  }
}

上面是服务端历史的简化示例。入库时 message_id/version/timeline_index 映射到 server_message_id/server_version/server_timeline_index;UI 使用同含义的统一消息对象。数据库独有的账号隔离键、可见标记和索引不必传到 RN;blocks 在数据库可序列化为 JSON,交给 UI 时恢复为对象数组。

表最小关键字段保存什么
conversation_messagesui_id、origin、blocks、server_*、local_sequence、updated_revision列表直接读取的消息内容;正式正文只有一份。
conversation_transcript_eventslive_session_ref、event_id、delta、group_id收到的字幕原料,按事件去重。
conversation_transcript_groupsgroup_id、ui_id、event_ids、content_state哪些事件组成哪条本地消息。
conversation_message_sourceslive_session_ref、event_id、server_message_refs哪个事件已由哪些正式消息确认。
conversation_sync_stateconversation_id、before_cursor、has_more历史翻页进度。

逻辑模型和查询结果合同保持一致;当前示例落地选择两端同名新表,物理索引由 Realm/Room 的查询计划决定。辅助表可存原始字幕,但不构成第二份正式消息。updated_revision 是本地变化版本,server_version 是服务端消息版本,两者不能混用。

blocks 是内容块数组,可扩展 image、audio、file、tool、form 等类型;类型未知时显示兼容占位,避免一个新卡片让整页解析失败。status 的具体枚举由产品协议统一,不能把生成结束、发送成功和历史确认混成一个状态。

05 / 列表读取、分页、刷新和定位

列表读取

服务端需要提供稳定的会话内排序键、消息版本和分页游标。当前示例落地使用 server_timeline_index 与 ui_id 排序、每页 50 条加 1 条探测;其他产品可按接口能力和预算配置。

向上翻页

接近已加载内容顶部时使用上一页游标请求更早消息。请求期间只显示轻量 loading;失败显示重试。成功后把消息放到窗口前面,保留当前可见消息身份和位置。

进入和重新进入

是否实时接收、是否在通话结束刷新,以及进入页面是否刷新,均由产品配置决定;当前示例落地选择进入/重进刷新,不增加轮询和通话结束自动刷新。

首屏定位

先准备目标消息附近的有限窗口,再测量定位;不能等待全部历史,也不能先显示明显错误位置再跳过去。

状态触发反馈
首次读取进入聊天页或重新进入。首屏准备指示,成功后显示。
向上加载接近顶部且has_more=true。顶部显示“正在加载”。
没有更多游标为空或探测行不足。内部结束,不强制显示文字。
失败网络、接口、身份、校验或数据库失败。保留已有消息,显示失败和Retry。

首次进入和首屏不足一屏

情况系统动作页面结果
有正式缓存先显示缓存,同时后台刷新最新历史。用户先看到已有消息,刷新成功后只更新变化行。
没有缓存创建列表并请求最新历史。显示首屏准备状态;成功确认无消息后才显示空态。
已有消息不足一屏首屏布局完成、视口变大或一页去重后,都重新判断是否补页;最多补有限页数。能滚动就继续准备上一页,游标耗尽或失败后停止。

回到最新

用户离开最新端后显示“有新消息 / 回到最新”入口;新消息只更新提示,不强行改滚动位置。点击后先准备最新窗口,再定位到最新端;成功后恢复跟随并清除提示。用户拖动期间,旧的异步判断不能把列表重新拉回最新。

为什么屏幕 20 行、数据库仍可以取 50 条

网络页、数据库页、已加载数据窗口和实际挂载窗口是四个尺度。一个屏幕假设 20 行,数据库取 50+1 条可以供当前屏幕和下一屏准备;真正挂载的仍只是附近行。当前示例落地用 50 条作为分页预算;通用方案只要求稳定游标和可配置窗口,不要求所有产品都使用 50。需要额外正文的消息按需补取,不能因此把整库载入。

从来没有同步历史,只有字幕

当前进程新增的字幕消息按 local_sequence 保留在已有会话缓存中,窗口可以按 50 条向前扩展;无需先请求历史才能上翻。本次进程重启后不再展示这些未确认 local,也不扫描旧 local 恢复。若纯字幕会话特别长,应约束内存窗口并保留本次创建的消息 ID,再按 ID 分页取正文;这属于后续容量优化,不增加 process_id。

分页并发和失效

同一会话的历史请求串行或复用同一个进行中请求;重复触发不发重复 HTTP。请求记录当前会话代次,返回时再次核对,旧页面或登出前的结果不能写进新页面。预加载结果写入缓存后,正式扩展窗口直接复用。游标未推进或多页只有重复数据时必须有停止上限及具体错误码,避免无限 loading。

正式区按服务端顺序排列,本地区按当前进程序号放在正式区之后;不发明可以比较两类消息的全局有序 ID。历史确认导致 local 移入正式区时,用 ui_id 和阅读锚点处理移动。跨重启是否展示未确认消息是产品选择;当前示例选择不展示,以避免本地和正式消息缺少全局顺序时产生歧义。

06 / 流畅性:少读、少算、少改位置

屏幕附近渲染

只挂载可见行和上下额外一屏,屏幕外用高度占位。

高度和位置缓存

先用估算高度,再测量并只修正受影响范围。

滚动期间不打断

用户拖动或减速时,不因测量和预加载立即改scroll offset。

阅读位置保持

只在插入上一页或前方高度变化时保留锚点。

有限预加载

接近顶部提前读一页或一个屏幕附近的数据,不整理一万条记录。

消息对象复用

同一ui_id且字段未变时复用对象,只有增长气泡替换对象。

一句话方案:把分页和测量放后台,把滚动位置交给当前手势,结构真正变化时才做有限修正。

原生 ScrollView、倒置列表和虚拟窗口是什么关系

原生 ScrollView 负责惯性、触摸和滚动偏移;InvertedList / TranscriptList 在它上面负责倒置坐标、高度估算、可见范围、占位、分页触发和定位。倒置只改变显示方向,不会自动减少挂载行数。iOS、Android 变换实现可以不同,对上层提供统一的“最新端 / 历史端”语义。普通列表优先复用成熟虚拟列表,自定义恢复实现需要验证坐标和原生位置保持的组合。

追底、阅读、定位三种控制状态

状态新内容到达退出条件
跟随最新新回复增长时跟随最新端,避免反复启动滚动动画。用户开始拖动即退出。
阅读历史保持阅读行,提示有新消息;不因后台回复强制追底。用户点击回到最新或回到阈值范围。
主动定位先挂载目标附近并测量,再发定位命令;到达后才确认成功。到达、失败或用户新拖动;超时不能当作成功。

阅读位置保持与“列表是否销毁”是两件事。即使实例一直保留,上一页插入、图片解码、字体变化仍可能改变前方高度;这时仍需保持阅读锚点。应只由一处统一决定位置修正,避免 JS 和原生同时纠偏。拖动代次用于丢弃旧的异步追底决定。

图片、卡片和键盘

图片尽早使用已知宽高比预留空间;图片加载后只重新测量该行。离屏复杂卡片暂停无用动画,正在编辑或播放的少量行可按稳定 ID 保留并设置上限;草稿与业务状态存在列表外部,不能依赖行一直挂载。键盘弹出改变可用视口,跟随最新时维持输入框和最新消息可见,阅读历史时保持原阅读行,避免键盘和列表两边重复补偿。

恢复版中的 400ms 新消息填充保持、1500ms 定位保护属于固定版本行为参考;同条消息的流式增量不重复延长保持时间。预取半屏、额外渲染一屏、缓存 600 条是 Demo/工程参数,需要测量后确定,不是通用默认值。

预加载附近突然停住:怎样判断原因

把手势/惯性、分页请求返回、数据库查询、合并、快照编码、UI 提交和位置命令放进同一时间线。如果停顿紧跟 offset 修正,先查位置控制冲突;如果没有滚动命令但 JS/UI 长时间无响应,再查全量计算、图片解码和大范围布局。不能见到滚动停下就一律称为“卡顿”。

07 / 主要场景

场景处理流程一句话方案
字幕连续到达按会话和角色找活动分组;相邻delta合并;更新同一local行;事件和分组落库。多个字幕增量拼成一条本地消息。
历史覆盖完整字幕按provider_event_ids找分组;校验覆盖;原local行改为server。确认消息更新原行,保留ui_id。
历史只覆盖部分字幕正式行保存服务端身份;未覆盖事件继续形成未确认的剩余本地消息行。按事件确认,余量继续显示。
历史先到、字幕后到先保存正式消息;迟到事件发现已覆盖则不改正式行;新事件开启新分组。正式消息优先,迟到字幕不能改回local。
主动发送文字生成client_request_id和ui_id;先写local pending;历史用请求ID确认。请求ID负责关联,文本只负责展示。
主动发送图片显示本地图片占位;上传得到asset_id/URL;历史确认原行。图片URL是资源地址,不能当消息ID。
重启查询新表origin=server;旧local保留但不加入快照。临时消息只在当前进程展示。
上一页失败保留当前窗口、游标和已有消息;Retry只重做失败页。失败不清空列表。
切Tab返回保留列表实例或状态;重新进入刷新最新历史并复用窗口锚点。页面重现不等于列表从零开始。
快速滚动预加载结果检查会话代次;滚动中不立即修正offset。异步结果只能更新数据,不能覆盖新操作。
图片加载后变高预留宽高比,复测变化行;历史区保持阅读锚点,最新端按跟随状态处理。改图片高度,不抢用户滚动。
跳到搜索结果按目标消息 ID 查找其附近窗口;挂载测量后滚动,未找到则明确失败。先拿到目标,再定位;超时不算成功。
卡片离屏又回来草稿、播放和表单状态放会话数据中;仅保留少量活跃行,离屏暂停无用动画。行可销毁,操作状态不能丢。
键盘弹出或旋转重新读取可用视口;保留当前阅读行或最新端,不重复计算键盘补偿。视口变化按当前阅读意图处理。
停止生成保留已收到的 blocks,标记当前生成请求失效;迟到增量不能继续修改这条消息。停止是生成控制,不是历史分页 Retry。
生成失败或重新生成失败保留原消息和错误状态;重新生成创建新请求或新版本,并按稳定身份更新。发送/生成重试与加载上一页重试分开。

08 / 消息上的操作交互

列表负责找到消息和保持阅读位置;具体动作由消息卡片或业务能力执行。所有操作都使用 ui_id + block_id,不使用当前数组下标;弹层关闭、媒体返回和异步结果到达后,继续使用原阅读锚点。

操作触发与结果失败或离屏处理
长按、选择和复制长按消息或文本块打开菜单;复制完成后保留当前列表位置。消息离屏时按稳定 ID重新定位,不依赖挂载实例。
图片预览点击图片打开预览;关闭后回到原消息,图片尺寸变化只复测对应行。加载失败显示图片级重试,不把整页置为加载失败。
文件、音频和工具卡片点击交给对应能力;播放/表单/草稿状态存在列表外部,卡片只读状态。卡片离屏可暂停动画;返回后按 ID恢复。
停止、失败和重新生成停止保留已到内容并使旧请求失效;重新生成走新请求或新版本。与历史分页 Retry 分开显示和记录。
引用、编辑、删除作为可选业务动作以稳定消息 ID发起;正式结果回来后更新/删除原行。删除或变更后用邻近行作为锚点,不按数组位置恢复。
无障碍和字体变化按实际消息顺序提供可读标签;字号、宽度、旋转变化后重建受影响高度。保持焦点所在 ui_id;加载反馈不能反复抢焦点。
一句话方案:消息卡片负责动作,列表负责身份、位置和生命周期;两者通过稳定 ID 连接。

09 / 错误、重试与生命周期

错误应归到具体动作:最新历史刷新、上一页、本地读取、正文补取或卡片操作。保留现有消息,结束对应 loading,Retry 只重复失败动作。没有更多数据是正常结束,不是错误。

首次进入状态显示规则
有缓存先显示缓存,后台刷新;刷新失败不遮住已有消息,只显示轻量提示和重试。
无缓存且正在请求显示首屏准备状态,不显示空态。
请求成功且没有消息确认服务端没有消息后显示空态。
无缓存且请求失败显示错误和 Retry,不能显示成“没有消息”。
场景处理机制必须记录
顶部出现通用加载失败区分刷新失败与分页失败;已有缓存仍可阅读,不清空列表。失败阶段、错误类别、HTTP 状态、可重试性、请求关联号。
网络超时或断网结束当前等待,按有限策略重试;用户主动 Retry 可重新尝试。持续时间、超时/取消的区别。
消息版本、内容或来源冲突原事务失败,保留此前一致数据;不能假装成功。校验代码和受影响数量,不记正文。
切 Tab / 后台 / 退出暂停观察与无用动画;保留同账号窗口;旧结果按代次失效。操作与取消原因,不能把主动取消显示为失败。
登出释放当前缓存与观察,数据库按账号隔离;后续迟到响应失效。隔离是否生效,不暴露账号或凭据。
一句话方案:错误可以使用简短文案,但日志必须保留出错环节和原因,验证也必须分别检查页面顶部与上一页区域。

10 / iOS、Android 和 React Native 分工

层职责共同约定
服务端接口返回正式消息、版本、顺序、来源事件和分页游标。message_id、version、timeline_index、client_request_id语义一致。
本地数据库保存统一消息和字幕关系,事务内更新覆盖关系。逻辑字段含义、空值规则和查询结果合同一致;当前示例落地选择两端同名新表,物理索引按数据库能力确定。
原生宿主负责账号、网络、数据库线程和RN Bridge。错误要保留阶段和错误码,不能全部变成同一句提示。
React Native渲染窗口、滚动、加载、重试和消息卡片。只消费统一ConversationRow,不依赖宿主内部字段。
列表组件控制可见范围、预加载、估算高度和定位。数据库和分页计算放后台,UI线程只做布局。

11 / 一句话方案速查

问题一句话方案
消息如何统一?字幕、历史、文字占位和图片占位都使用同一展示消息模型,来源关系另存。
字幕如何合并?按会话、角色和时间窗口把多个delta合并为一条local消息。
如何确认正式消息?按服务端消息ID和来源事件确认,完整确认时原地更新为server。
是否保存字幕?保存原始事件和分组关系,避免靠全文猜测。
如何分页?服务端提供稳定顺序和游标;页大小、探测条数和预取距离按产品预算配置。
如何预加载?接近顶部提前读取一页或一个屏幕附近的数据。
如何保持滚动?用稳定ui_id做锚点,必要时有限修正,手势期间不抢控制权。
如何处理失败?保留已显示消息,区分首次加载、上一页加载和正文补取错误。
如何跨重启?跨重启恢复策略按产品选择;当前示例选择只恢复正式消息。
如何支持双端?统一字段和状态语义,Realm、Room和RN分别实现各自职责。

12 / 依据、取舍和当前示例边界

本方案针对“与 AI 对话的消息列表”,不设计首页 Bot 目录。公开资料检索日期为 2026-11-05。不同 AI 消息产品的功能和技术实现不能混为一谈。

依据层级可确认的事实不能据此推出什么
公开产品官方文档同一消息区承载文字、附件、工具活动和交互卡片;手机草稿按会话保留;支持历史搜索后跳转。消息与协作、移动端。官方未公开列表算法、页大小、缓存容量和底层数据库。
公开产品官方排障资料聊天历史独立于云端电脑;手机和桌面共享会话;隐藏不删除历史;反馈可附请求 ID。排障说明。不能据此证明它采用本方案的 local/server 字段或当前字幕确认规则。
React Native 官方文档ScrollView 本身会创建全部子项;虚拟列表通过有限窗口及空白占位减少开销。ScrollView、VirtualizedList。只换成 ScrollView 不会自动获得虚拟化,也不能保证滚动更快。
本次设计决定统一消息模型、当前进程 local 展示、重启仅 server、50 条分页、未来发送字段支持。这些是当前示例的选择,不宣称是公开产品的内部存储设计。

需要保留的取舍

当前进程 local 展示避免跨重启排序歧义,但代价是尚未确认内容在重启后暂不展示,离线重启也不能恢复这些气泡。这不是通用聊天产品的唯一选择;如果未来要求离线待发恢复,需要另行设计持久发送队列、重试和跨端顺序。

验收看行为,不只看构建

至少覆盖:空库首次加载、正式缓存冷启动、纯字幕上翻、完整与部分确认、正式先到、图片高度变化、流式中读历史、快滑遇预取、跨 Tab 返回、旧响应迟到、游标耗尽、每个加载环节失败及原地重试。记录数据库读取数、处理耗时、挂载行数、帧间隔和非主动滚动次数;测试和实机结果分别记录。

当前遗留风险:本次真机出现过顶部通用加载失败提示,根因尚未确认;真机曾通过分页、切换和冷启动不代表该偶发错误已经解决。真实实时会话中的字幕与历史并发、文字和图片真实发送尚未完成完整设备验收。