首页
/ Rocket.Chat 统一消息历史接口 `GET /v1/rooms.history`:跨房间类型的历史记录加载与游标分页实战解析

Rocket.Chat 统一消息历史接口 `GET /v1/rooms.history`:跨房间类型的历史记录加载与游标分页实战解析

2026-09-08 22:18:29作者:郜逊炳

在 Rocket.Chat 中,历史消息的 REST 化读取此前依赖 channels.historygroups.historyim.historydm.history 这一组按房间类型拆分、各走独立路由的端点。本篇文章围绕新版变更集 .changeset/rest-rooms-history.md 引入的 GET /v1/rooms.history 端点展开:它用一个统一路由覆盖频道、私信、讨论等所有房间类型,并支持在开启 Accounts_AllowAnonymousRead 时匿名读取公开频道历史。读完本文,你将掌握该端点的请求参数与校验规则、游标分页与未读消息计算的底层实现、权限判定链路,以及如何在客户端使用同一端点稳定加载历史消息。

一、为什么需要一个新的历史记录端点

在 Rocket.Chat 既有代码库中,房间类型与历史接口长期呈一一对应的关系:channels.history 对应公开频道、groups.history 对应私有群组、im.history/dm.history 对应私信会话。这类设计带来的直接问题是:

  • 调用方必须预先知道目标房间属于哪种类型,才能拼出正确的路由;
  • 无法用一条通用逻辑处理“任取一个房间读历史”的场景(如管理后台、机器人、跨类型聚合查询);
  • 匿名访问公开频道历史需要额外打通各类型端点的鉴权路径。

GET /v1/rooms.history 正是为解决这些问题而新增的统一历史记录接口。它接受一个 roomId,即可读取任意类型房间的消息历史;当系统开启 Accounts_AllowAnonymousRead 时,还能以未登录(匿名)身份读取公开频道的历史消息。从变更集声明看,该功能同时涉及 @rocket.chat/rest-typings(类型与请求校验 Schema)与 @rocket.chat/meteor(服务端实现)两个包的 minor 版本变更。

与旧版本 DDP 方法的关系

接口实现处的注释指出:其 count 默认取 20,是为了“匹配被替换的 DDP 方法”,而非沿用通用的 API_Default_Count 设置(但仍受 API_Upper_Count_Limit 上限约束)。也就是说,这个 REST 端点实际是把原先以 Meteor DDP 方式提供的“房间历史加载”能力迁移到了 HTTP REST 层,并统一了行为,详见 服务端实现 中的默认值处理。

二、请求格式与参数说明

端点的路由定义与查询参数校验位于 服务端路由注册,其查询参数的类型声明和 JSON Schema 校验规则定义在 packages/rest-typings/src/v1/rooms.ts。核心请求参数如下:

参数 类型 必填 说明
roomId string 目标房间 ID,Schema 要求 minLength: 1
next string 游标:向“更新”方向翻页(取比该游标更新的消息),不可与 previous 同时给出
previous string 游标:向“更旧”方向翻页,不可与 next 同时给出
lastSeen string ISO 8601 日期时间字符串,仅用于定位“未读分隔线”,不作为翻页边界
count integer 本次返回的最大条数,minimum: 1;缺省按 20 处理并受 API_Upper_Count_Limit 上限钳制
showThreadMessages boolean 是否把线程回复并入历史结果,默认 true

需要特别留意 Schema 上的两条硬约束:

  1. required: ['roomId'] —— 其余全部可选;
  2. additionalProperties: false —— 出现任何未声明字段(如拼写错误的 room_id)都会被校验层直接拒绝,返回 400。

请求校验由 isRoomsHistoryProps = ajvQuery.compile<RoomsHistoryProps>(RoomsHistorySchema) 完成(见 类型与校验),这意味着该端点的入参在编译期(TypeScript 类型)与运行期(AJV 校验)两侧都受到严格约束。

一个典型的首次请求示例:

GET /api/v1/rooms.history?roomId=GENERAL&count=50

首次请求不携带任何游标,接口返回最新的一页消息(见下文游标语义)。翻页时使用响应体 cursor 字段回填 nextprevious

三、响应结构解析

端点对 200 成功响应的约束由 roomsHistoryResponseSchema 定义(见 响应 Schema 定义),同时也注册了 400 / 401 / 403 / 404 等错误响应。成功响应结构如下:

{
  "messages": [
    /* 数组,元素为 IMessage,若调用方已登录则已按用户做脱敏/过滤(normalizeMessagesForUser) */
  ],
  "cursor": {
    "next": "1710000000000",
    "previous": "1709999000000"
  },
  "firstUnread": {},
  "unreadNotLoaded": 12,
  "success": true
}
字段 必填 说明
messages 本次返回的消息列表(IMessage 数组),按时间由新到旧排列
cursor.next 指向更新消息的游标(时间戳毫秒字符串);没有更多新消息时为 null
cursor.previous 指向更旧消息的游标;首页(最新端)向前翻页为空时为 null
firstUnread 若指定了 lastSeen,返回该时间点之后的第一条未读消息
unreadNotLoaded lastSeen 起、当前页未包含的未读消息数量
success 恒为 true

messagescursorsuccess 三个字段在 Schema 中被标记为必填,firstUnreadunreadNotLoaded 为可选,整个对象不允许出现额外字段。响应体的实际构造逻辑来自工具函数 loadRoomHistory 的返回值(见 loadRoomHistory.ts)。

四、鉴权与权限判定链路

端点配置使用 authOrAnonRequired: true(见 端点注册),即“登录认证或匿名访问”二选一。其鉴权流程可以归纳为三层:

  1. 身份层(中间件):请求要么携带有效登录态,要么系统已开启 Accounts_AllowAnonymousRead 从而放行匿名请求——实现处的注释明确说明“匿名读取已由中间件基于 Accounts_AllowAnonymousRead 先行把关”。
  2. 房间可访问性:处理器通过 Rooms.findOneById 加载房间,房间不存在返回 404;随后调用 canAccessRoomAsync(room, this.user) 做细粒度访问控制,失败返回 403(见 处理器逻辑)。
  3. 公开频道预览权限:对已登录但未订阅该频道的用户,若其没有 preview-c-room 权限,则同样返回 403;已订阅用户不受此限制(见 预览权限校验)。

因此,能够访问该接口的典型身份包括:房间成员、持有 preview-c-room 的非订阅用户,以及在 Accounts_AllowAnonymousRead 开启时访问公开频道的匿名访客。综合下来,接口对外暴露 200(成功)、400(参数非法)、401(未认证且不允许匿名)、403(无权限)、404(房间不存在)五类状态码。

五、游标分页机制:cursor 的生成、解码与方向语义

rooms.history 采用基于时间戳的游标分页,而非传统的 offset/limit。所有游标逻辑都封装在 loadRoomHistory.ts 中。

游标的编解码

游标本身只是消息时间戳的毫秒数转字符串:

  • encodeHistoryCursor(ts) 返回 `${ts.getTime()}`
  • decodeHistoryCursor(cursor) 先要求纯数字格式,再校验是否可解析为合法日期,任一环节失败即抛出 error-invalid-cursor

两个方向互斥

loadRoomHistory 的第一步就是断言 nextprevious 不能同时出现,否则抛出 error-cursor-conflict。方向由游标唯一决定:

  • 携带 next:表示沿时间轴“向前/更新”翻页,查询区间为 (next 时间戳, FAR_FUTURE]
  • 携带 previous:表示沿时间轴“向后/更旧”翻页,查询区间为 (-∞, previous 时间戳)
  • 两者都不带:从房间最新消息开始取第一页。

源码中使用 FAR_FUTURE = new Date(8640000000000000)(即 JS Date 的上界)作为时间范围查询的封闭上界,注释说明这是为了防止“客户端打上的未来时间戳”在开区间查询中被意外丢弃(见 FAR_FUTURE 定义)。

排序、翻页探测与 _id 决胜

实现细节上非常讲究:

  • 查询排序方向与翻页方向相反(nextts 升序、否则降序),并多取一条limit: count + 1)用于探测是否还有后续页;
  • 取回记录后先在内存中按“ts 相同则用 _id 做决胜”的规则重排。之所以需要这一条,是因为 MongoDB 的 { rid, ts, _updatedAt } 复合索引无法支撑 { ts, _id } 排序,若不加决胜,同一时间戳下的消息返回顺序将不确定(见 排序与决胜逻辑);
  • 若发现确实存在更多页,则把多取的最后一条弹掉;
  • 携带 next 时,最终将整页反转,因此无论朝哪个方向翻页,返回的 messages 都统一为“由新到旧”排列(见 loadRoomHistory.ts)。

cursor 中各字段何时为 null

结果对象中的 hasNewer/hasOlder 逻辑决定了游标是否为空:

  • 携带 next 翻页时,hasOlder 恒为 true(说明更旧一侧总还有数据),而 hasNewer 取决于本页是否被截断(是否真的还有更新消息);
  • 携带 previous 翻页时相反;
  • 首页因从最新端起步,hasNewer 由截断标志决定、hasOlder 由是否存在更早消息决定。

最终 next/previous 要么是对应方向最新/最旧一条消息的时间戳游标,要么是 null(表示该方向已到尽头)。客户端判断“是否还能继续翻”只需看 cursor.next/cursor.previous 是否为 null

六、lastSeen 与未读定位:不破坏翻页的“未读分隔”

这是 rooms.history 区别于传统分页接口的另一大能力:在返回历史页的同时附带未读定位信息

loadRoomHistory 注释解释得很清楚——lastSeen 只用来“摆放未读分隔线”,如果拿它当翻页边界,页面会在标记处被截断,这正是分房间类型的旧 *.history 端点无法提供该能力的原因(见 设计注释)。

具体算法(computeUnread,见 未读计算)在以下条件全部满足时生效:

  • 请求携带了 lastSeen 且可解析为合法时间;
  • 当前页存在最旧一条消息(oldest);
  • oldest.ts 严格晚于 lastSeen(即当前加载窗口确实跨过了未读边界)。

满足时,接口会并行执行两条查询:取 (lastSeen, oldest.ts] 区间内按 ts 升序的第一条作为 firstUnread,并统计该区间内消息总数作为 unreadNotLoaded。这样前端可以在页面中精确渲染“从这里开始是你的未读”的提示,同时不会影响原有翻页游标的连续性。

七、服务端的数据可见性与系统消息过滤

历史查询并非简单地把房间内全部消息倒出来,服务端做了两层过滤,均由 loadRoomHistory 统一处理:

  1. 可见性过滤:调用 Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes / Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes 系列查询,只读取“可见”消息;
  2. 系统消息过滤:通过 getHiddenSystemMessages(room, settings.get('Hide_System_Messages')) 计算当前设置 Hide_System_Messages 下需要隐藏的系统消息类型集合,并从结果中剔除(见 loadRoomHistory.ts)。

对已登录用户,返回前还会经 normalizeMessagesForUser(records, userId) 处理,按用户身份对消息做脱敏与展示级适配(例如依据用户的订阅情况过滤掉无权看到的消息内容)。

八、在客户端与类型层使用该端点

类型定义

RoomsHistoryProps 与配套 Schema 已并入 @rocket.chat/rest-typings,并作为 GET 参数类型挂载到 Endpoints 接口上(见 类型声明)。升级 @rocket.chat/rest-typings 到含本次 minor 变更的版本后,sdk.rest.get('/v1/rooms.history', ...) 即可获得完整的请求/响应类型推导与运行期校验。

客户端调用示例

Rocket.Chat 客户端自身的房间历史管理器 RoomHistoryManager 已经切换到该端点。从 RoomHistoryManager.ts 与同文件 第二处调用 可以看到统一调用形态:

const result = await sdk.rest.get('/v1/rooms.history', {
  roomId,
  count,
  lastSeen,
  next: nextCursor ?? undefined,
});

翻页时只需把上一次响应 cursor.next(向更新方向)或 cursor.previous(向更旧方向)透传回去即可,服务端会按第五节描述的规则处理方向与排序,客户端无需感知房间类型差异。

九、可用性前提与迁移注意

  • 版本要求:该端点随 @rocket.chat/rest-typings@rocket.chat/meteor 的 minor 版本更新发布,使用时需确保服务端与类型包均已升级到包含本次变更的版本。
  • 匿名读取是特性而非默认:匿名访问公开频道历史仅在系统设置 Accounts_AllowAnonymousRead 开启时生效;未开启时匿名请求会停留在 401 层,不会进入房间访问判断。
  • 参数严格性additionalProperties: false 意味着所有扩展参数都会导致 400;next/previous 互斥、游标必须为合法毫秒时间戳字符串,违反任一约束都会触发对应的校验错误。
  • 统一替换路径:新接入历史消息功能的场景应优先使用 rooms.history,以同时获得跨房间类型支持、游标翻页与未读定位能力;既有的按类型端点仍可用于向后兼容场景。

以上分析与参数默认值均直接对照本仓库中的 变更集路由与权限实现核心加载逻辑REST 类型定义,可作为深度阅读上述源码的导览起点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391