Skip to content

ROMP 聊天 API

ROMP(Real-time Open Messaging Protocol)聊天系统接口,支持私聊会话、消息收发、AI Agent 集成、推送 Token 管理和 Novu 认证。

路由前缀/romp

源码apps/backend/src/routes/romp/(index.ts、conversations.ts、messages.ts、files.ts、push.ts、novu-auth.ts、webhook-trigger.ts)

参考文档:内部文档 docs/romp/api-reference.md


认证

所有端点需要 Authorization 头:

Authorization: Bearer <TOKEN>

支持三种认证方式:

方式Token 格式origin 值适用场景
JWT(用户登录态)Supabase JWTclient前端/App 用户操作
API Keywn- 前缀密钥api服务端/机器人调用
Agent Token短期 JWT(有效期 10 分钟)apiAI Agent 在会话中回复消息

Agent Token 由后端在触发 Webhook 时自动签发,仅允许访问消息和输入状态相关端点,且限定在触发它的会话范围内。

部分端点(推送注册/注销、Novu 认证)仅支持 JWT。


错误格式

json
{
  "error": {
    "code": "invalid_request",
    "message": "conversation_id is required",
    "param": "conversation_id"
  }
}
字段类型必填说明
codestring机器可读错误码
messagestring人类可读错误描述
paramstring出错的参数名

错误码一览:

codeHTTP含义
unauthorized401未认证或 Token 无效/过期
invalid_request400请求参数错误(含文件大小超限、MIME 类型不允许等)
forbidden403无权限(非参与者等)
not_found404资源不存在
internal_error500服务端错误

会话端点

1. 创建私聊会话

POST /romp/v1/conversations

创建与另一个用户的私聊。若已存在则直接返回(幂等)。

请求体:

字段类型必填说明
typestring固定为 "private"
participantsstring[]对方的 user_id,恰好 1 个
json
{
  "type": "private",
  "participants": ["<对方的 user_id>"]
}

响应 201 Created(新建)或 200 OK(已存在):

json
{
  "id": "conv-uuid",
  "type": "private",
  "title": null,
  "participants": [
    {
      "id": "user-a-uuid",
      "type": "user",
      "name": "张三",
      "avatar": "https://example.com/avatar-a.jpg",
      "role": "owner"
    },
    {
      "id": "user-b-uuid",
      "type": "user",
      "name": "李四",
      "avatar": "https://example.com/avatar-b.jpg",
      "role": "member"
    }
  ],
  "created_at": "2026-02-20T10:00:00.000Z",
  "updated_at": "2026-02-20T10:00:00.000Z"
}

错误码:

场景HTTPcode
type 不是 "private"400invalid_request
participants 长度不为 1400invalid_request
不能和自己创建私聊400invalid_request
对方用户不存在404not_found

2. 获取会话列表

GET /romp/v1/conversations

获取当前用户参与的所有会话,按最近活跃时间降序排列。

查询参数:

参数类型必填默认说明
limitnumber20每页数量,最大 50
cursorstring--上一页最后一条的 id

响应 200 OK

json
{
  "data": [
    {
      "id": "conv-uuid",
      "type": "private",
      "title": null,
      "peer": {
        "id": "user-b-uuid",
        "type": "user",
        "name": "李四",
        "avatar": "https://example.com/avatar-b.jpg"
      },
      "last_message": {
        "id": "msg-uuid",
        "sender_name": "李四",
        "summary": "你好,在吗?",
        "created_at": "2026-02-20T10:30:00.000Z"
      },
      "unread_count": 3,
      "is_pinned": false,
      "pinned_at": null,
      "updated_at": "2026-02-20T10:30:00.000Z"
    }
  ],
  "has_more": true,
  "cursor": "conv-uuid"
}

last_message.summary 生成规则:

summary 基于消息第一个 part 生成:

parts[0].kindsummary
text文本内容(超过 50 字截断并加 ...
thinking[思考中...]
image[图片]
audio[语音]
video[视频]
file[文件] 文件名
其他[消息]

若消息包含多个 part,summary 仅反映第一个 part 的内容。混合消息(如图文混排)的摘要以首个 part 为准。

排序规则:

会话列表按以下优先级排序:

  1. 置顶会话排在最前(按 pinned_at 降序)
  2. 非置顶会话按 updated_at 降序

新增字段:

字段类型说明
is_pinnedboolean当前用户是否置顶了该会话
pinned_atstring | null置顶时间(ISO 8601),未置顶为 null

3. 置顶会话

POST /romp/v1/conversations/:id/pin

将指定会话置顶。每个用户独立管理置顶状态,不影响其他参与者。

路径参数:

参数说明
id会话 UUID

响应 200 OK

json
{
  "ok": true,
  "pinned_at": "2026-03-12T10:00:00.000Z"
}

错误码:

场景HTTPcode
会话不存在404not_found
不是会话参与者403forbidden

4. 取消置顶

DELETE /romp/v1/conversations/:id/pin

取消指定会话的置顶状态。

路径参数:

参数说明
id会话 UUID

响应 200 OK

json
{ "ok": true }

错误码:

场景HTTPcode
会话不存在404not_found
不是会话参与者403forbidden

5. 标记已读

POST /romp/v1/conversations/:id/read

将指定会话标记为已读(到某条消息为止)。只能向前推进,不会倒退。

路径参数:

参数说明
id会话 UUID

请求体:

字段类型必填说明
message_idstring已读到的最新消息 ID

响应 200 OK

json
{
  "last_read_message_id": "msg-uuid"
}

行为说明:

  • 如果 message_id 比当前已读位置旧,不会倒退,返回当前值
  • 如果 message_id 更新,向前推进到该消息
  • 原子操作,行级锁保证并发安全

错误码:

场景HTTPcode
message_id 缺失400invalid_request
消息不存在或不属于此会话400invalid_request
会话不存在404not_found
不是会话参与者403forbidden

6. 发送输入状态

POST /romp/v1/conversations/:id/typing

向指定会话广播当前用户的输入状态。通过 Supabase Realtime Broadcast 推送给会话中的其他参与者。

路径参数:

参数说明
id会话 UUID

请求体:

字段类型必填说明
statusstring输入状态枚举值

状态枚举:

含义
thinkingAI 正在思考
typing用户/AI 正在输入
tool_callingAI 正在调用工具
idle空闲(停止输入)

服务端节流: 同一用户在同一会话中发送相同状态,1 秒内仅广播一次。重复请求不会报错,返回 { "ok": true } 但不触发广播。

响应 200 OK

json
{ "ok": true }

Broadcast 事件格式(Realtime 推送):

json
{
  "event": "typing",
  "payload": {
    "user_id": "user-a-uuid",
    "status": "typing",
    "user_name": "张三"
  }
}

频道名:romp-typing:<conversation_id>

错误码:

场景HTTPcode
status 缺失或无效400invalid_request
会话不存在404not_found
不是会话参与者403forbidden

消息端点

7. 发送消息

POST /romp/v1/messages

向指定会话发送一条消息。

请求体:

字段类型必填说明
conversation_idstring目标会话 ID
partsPart[]消息内容,至少 1 个 Part
reply_tostring回复的消息 ID
metadataobject自定义元数据,透传存储
json
{
  "conversation_id": "conv-uuid",
  "parts": [
    { "kind": "text", "text": "你好" }
  ],
  "reply_to": null,
  "metadata": {
    "client_msg_id": "local-uuid"
  }
}

Part 类型:

kind必填字段示例
texttext: string(非空){"kind":"text","text":"你好"}
thinkingcontent: string(非空){"kind":"thinking","content":"让我想想..."}
imagefile_id: string{"kind":"image","file_id":"uuid","url":"...","name":"photo.jpg"}
audiofile_id: string{"kind":"audio","file_id":"uuid","url":"...","duration":5.2}
videofile_id: string{"kind":"video","file_id":"uuid","url":"..."}
filefile_id: string{"kind":"file","file_id":"uuid","url":"...","name":"report.pdf","size":102400}
其他 kind无强制校验直接存储(前向兼容)

多媒体 part(image/audio/video/file)中的 url 为签名 URL,有效期 1 小时。过期后可通过「刷新文件 URL」端点获取新 URL。

metadata.client_msg_id(推荐):

客户端发送前生成一个本地 UUID 放入 metadata.client_msg_id,用于防重复显示和乐观 UI。

响应 201 Created

json
{
  "id": "msg-uuid",
  "conversation_id": "conv-uuid",
  "sender": {
    "id": "user-a-uuid",
    "type": "user",
    "name": "张三",
    "avatar": "https://example.com/avatar-a.jpg"
  },
  "origin": "client",
  "parts": [
    { "kind": "text", "text": "你好" }
  ],
  "reply_to": null,
  "metadata": { "client_msg_id": "local-uuid" },
  "edited_at": null,
  "created_at": "2026-02-20T10:30:00.000Z"
}

origin 字段:

含义
client通过 JWT 认证发送(用户在 App 中操作)
api通过 API Key 或 Agent Token 发送(服务端/机器人/AI Agent)
system系统自动发送的消息

副作用:

  • 会话的 updated_at 自动更新
  • 异步触发推送通知(发送者以外的所有参与者):Novu(In-App + FCM)、JPush、EMAS、Web Push
  • 新消息通过 Supabase Realtime 推送给订阅者
  • 若会话中有 AI Agent 参与者且发送者非 Agent,异步触发 Agent Webhook

错误码:

场景HTTPcode
conversation_id 缺失400invalid_request
parts 为空或格式错误400invalid_request
会话不存在404not_found
不是会话参与者403forbidden
reply_to 消息不存在或不属于此会话400invalid_request

8. 获取消息历史

GET /romp/v1/messages

获取指定会话的消息历史,支持双向游标分页。

查询参数:

参数类型必填默认说明
conversation_idstring--会话 ID
limitnumber50每页数量,最大 100
beforestring--获取此消息之前的(更早的)
afterstring--获取此消息之后的(更新的)

beforeafter 互斥,不可同时使用。

响应 200 OK

json
{
  "data": [
    {
      "id": "msg-uuid",
      "conversation_id": "conv-uuid",
      "sender": {
        "id": "user-b-uuid",
        "type": "user",
        "name": "李四",
        "avatar": "https://example.com/avatar-b.jpg"
      },
      "origin": "client",
      "parts": [{ "kind": "text", "text": "你好!" }],
      "reply_to": null,
      "metadata": {},
      "edited_at": null,
      "created_at": "2026-02-20T10:31:00.000Z"
    }
  ],
  "has_more": false,
  "cursor": null
}

排序与翻页:

模式排序用途
默认(无游标)时间倒序(最新在前)进入聊天室加载最新消息
before=<id>时间倒序上滑加载更早消息
after=<id>时间正序下滑加载更新消息

错误码:

场景HTTPcode
conversation_id 缺失400invalid_request
before 和 after 同时传400invalid_request
游标消息不存在400invalid_request
会话不存在404not_found
不是会话参与者403forbidden

9. 编辑消息

PATCH /romp/v1/messages/:id

编辑一条已发送的消息。仅允许修改文本类 partstextthinking 等),不允许编辑包含文件附件(image/audio/video/file)的 parts。

路径参数:

参数说明
id消息 UUID

请求体:

字段类型必填说明
partsPart[]新的消息内容,至少 1 个 Part,仅允许文本类
json
{
  "parts": [
    { "kind": "text", "text": "修改后的内容" }
  ]
}

响应 200 OK

json
{
  "id": "msg-uuid",
  "conversation_id": "conv-uuid",
  "sender": {
    "id": "user-a-uuid",
    "type": "user",
    "name": "张三",
    "avatar": "https://example.com/avatar-a.jpg"
  },
  "origin": "client",
  "parts": [
    { "kind": "text", "text": "修改后的内容" }
  ],
  "reply_to": null,
  "metadata": { "client_msg_id": "local-uuid" },
  "edited_at": "2026-02-20T10:35:00.000Z",
  "created_at": "2026-02-20T10:30:00.000Z"
}

edited_at:消息被编辑后自动设置为编辑时间(ISO 8601),未编辑的消息此字段为 null

约束说明:

  • 只能编辑自己发送的消息
  • 必须仍是会话参与者
  • parts 中不允许包含 image/audio/video/file kind
  • 编辑不会触发推送通知

错误码:

场景HTTPcode
parts 为空或格式错误400invalid_request
parts 包含文件类 kind400invalid_request
消息不存在404not_found
非消息发送者403forbidden
不再是会话参与者403forbidden

推送端点

10. 注册推送 Token

POST /romp/v1/push/register

注册设备推送 Token,用于 App 端接收原生推送通知。仅 JWT 认证

请求体:

字段类型必填说明
tokenstring推送 token
platformstring移动端:"ios""android";Web Push:"web"
providerstring推送通道(默认 "fcm"),可选值:fcmapnsexpojpushemasweb-push
integrationIdentifierstringNovu FCM 集成标识,仅 provider="fcm" 时有意义
subscriptionKeysobjectWeb Push 订阅密钥(仅 provider="web-push" 时必填)

subscriptionKeys 结构(Web Push):

字段类型说明
p256dhstringP-256 ECDH 公钥
authstring认证密钥

provider-platform 组合约束:

provider允许的 platform说明
fcmios, android通用
apnsios, android通用
expoios, android通用
jpushandroid仅 Android
emasios, android阿里云 EMAS
web-pushweb浏览器推送

行为说明:

  • 幂等:同一 token 重复注册仅更新 updated_at
  • Token 迁移:若 token 已属于其他用户,原子迁移到当前用户
  • Novu 同步仅在 provider="fcm" 时执行

响应 200 OK

json
{ "success": true }

错误码:

场景HTTPcode
非 JWT 认证403forbidden
token 缺失400invalid_request
platform 无效400invalid_request
provider 无效400invalid_request
web-push 缺少 subscriptionKeys400invalid_request
web-push platform 非 "web"400invalid_request

11. 注销推送 Token

POST /romp/v1/push/unregister
DELETE /romp/v1/push/register

注销设备推送 Token。仅 JWT 认证,幂等。

支持两种方式(DELETE 为兼容别名,部分客户端/网关对 DELETE body 支持较差)。

POST 请求体:

字段类型必填说明
tokenstring要注销的推送 token

DELETE 请求: body 或 ?token=xxx query param

响应 200 OK

json
{ "success": true }

错误码:

场景HTTPcode
非 JWT 认证403forbidden
token 缺失400invalid_request

Novu 认证端点

12. 获取 Novu subscriberHash

GET /romp/v1/novu/subscriber-hash

返回当前用户的 HMAC subscriberHash,供 Web/App 端 Novu SDK 初始化时传入,启用安全校验。仅 JWT 认证

HMAC = SHA256(subscriberId, NOVU_API_KEY)

响应 200 OK

json
{ "subscriberHash": "hex-hmac-sha256-string" }

错误码:

场景HTTPcode
非 JWT 认证403forbidden
Novu 未配置503internal_error

文件端点

13. 上传文件

POST /romp/v1/upload
Content-Type: multipart/form-data

上传多媒体文件,用于在消息中发送图片、语音、附件等。仅 JWT 认证

请求参数(form-data):

字段类型必填说明
fileFile文件
conversation_idstring目标会话 ID
purposestringmessage_image / message_audio / message_file

文件大小限制:

purpose限制允许的 MIME 类型
message_image20 MBimage/jpeg, image/png, image/gif, image/webp
message_audio5 MBaudio/mp4, audio/aac, audio/mpeg, audio/webm, audio/ogg, audio/wav, audio/m4a, audio/x-m4a
message_file50 MB不限制
未指定50 MB不限制

MIME 规范化:上传时后端会自动规范化 MIME 类型——去除参数部分并转为小写。例如浏览器 MediaRecorder 输出的 audio/webm;codecs=opus 会被规范化为 audio/webm 后再做白名单匹配。

响应 200 OK

json
{
  "file_id": "uuid",
  "name": "photo.jpg",
  "mime_type": "image/jpeg",
  "size": 204800,
  "url": "签名 URL(有效期 1 小时)",
  "expires_at": "2026-03-09T01:00:00.000Z"
}

错误码:

场景HTTPcode
非 JWT 认证403forbidden
未上传文件400invalid_request
conversation_id 缺失400invalid_request
文件超过大小限制400file_too_large
MIME 类型不允许400invalid_file_type
会话不存在404not_found
不是会话参与者403forbidden

14. 刷新文件 URL

GET /romp/v1/files/:fileId/url

获取文件的新签名 URL(有效期 1 小时)。用于文件 URL 过期后重新获取可访问的下载地址。

路径参数:

参数说明
fileId文件 UUID

响应 200 OK

json
{
  "url": "新签名 URL",
  "expires_at": "2026-03-09T01:00:00.000Z"
}

错误码:

场景HTTPcode
文件不存在404not_found
无权访问该文件403forbidden

15. 获取文件预览

GET /romp/v1/files/:fileId/preview

获取文件的缩略图预览 URL。适用于图片类文件的列表展示场景。

路径参数:

参数说明
fileId文件 UUID

响应 200 OK

json
{
  "url": "优先缩略图,无则原图",
  "original_url": "原图 URL",
  "thumbnail_url": "缩略图 URL 或 null",
  "expires_at": "2026-03-09T01:00:00.000Z"
}

错误码:

场景HTTPcode
文件不存在404not_found
无权访问该文件403forbidden

数据模型

ProfileType

typescript
type ProfileType = 'user' | 'agent' | 'system'

Participant 对象

typescript
interface Participant {
  id: string         // user_id
  type: ProfileType  // 用户类型(user/agent/system)
  name: string | null
  avatar: string | null
  role: 'owner' | 'member'
}

Sender / Peer 对象

typescript
interface Sender {
  id: string         // user_id
  type: ProfileType  // 用户类型(user/agent/system)
  name: string | null
  avatar: string | null
}

Message 对象

typescript
interface Message {
  id: string
  conversation_id: string
  sender: Sender
  origin: 'client' | 'api' | 'system'
  parts: Part[]
  reply_to: string | null
  metadata: Record<string, unknown>
  edited_at: string | null  // ISO 8601,未编辑时为 null
  created_at: string        // ISO 8601
}

Part 类型

typescript
type Part =
  | { kind: 'text'; text: string }
  | { kind: 'thinking'; content: string }
  | { kind: 'image'; file_id: string; url?: string; name?: string; mime_type?: string; size?: number; width?: number; height?: number }
  | { kind: 'audio'; file_id: string; url?: string; name?: string; mime_type?: string; size?: number; duration?: number; waveform?: number[] }
  | { kind: 'video'; file_id: string; url?: string; name?: string; mime_type?: string; size?: number; duration?: number; width?: number; height?: number }
  | { kind: 'file'; file_id: string; url?: string; name?: string; mime_type?: string; size?: number }
  | { kind: string; [key: string]: unknown }  // 前向兼容

LastMessage 对象

typescript
interface LastMessage {
  id: string
  sender_name: string
  summary: string      // 摘要文本
  created_at: string
}

相关文档

文档说明
ROMP API Reference(内部文档)完整参考文档
实时消息订阅(内部文档)Supabase Realtime 订阅方式
App 推送集成指南(内部文档)iOS/Android/Flutter 推送接入指南

AI Workflow Editor