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 JWT | client | 前端/App 用户操作 |
| API Key | wn- 前缀密钥 | api | 服务端/机器人调用 |
| Agent Token | 短期 JWT(有效期 10 分钟) | api | AI Agent 在会话中回复消息 |
Agent Token 由后端在触发 Webhook 时自动签发,仅允许访问消息和输入状态相关端点,且限定在触发它的会话范围内。
部分端点(推送注册/注销、Novu 认证)仅支持 JWT。
错误格式
{
"error": {
"code": "invalid_request",
"message": "conversation_id is required",
"param": "conversation_id"
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
code | string | 是 | 机器可读错误码 |
message | string | 是 | 人类可读错误描述 |
param | string | 否 | 出错的参数名 |
错误码一览:
| code | HTTP | 含义 |
|---|---|---|
unauthorized | 401 | 未认证或 Token 无效/过期 |
invalid_request | 400 | 请求参数错误(含文件大小超限、MIME 类型不允许等) |
forbidden | 403 | 无权限(非参与者等) |
not_found | 404 | 资源不存在 |
internal_error | 500 | 服务端错误 |
会话端点
1. 创建私聊会话
POST /romp/v1/conversations创建与另一个用户的私聊。若已存在则直接返回(幂等)。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 固定为 "private" |
participants | string[] | 是 | 对方的 user_id,恰好 1 个 |
{
"type": "private",
"participants": ["<对方的 user_id>"]
}响应 201 Created(新建)或 200 OK(已存在):
{
"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"
}错误码:
| 场景 | HTTP | code |
|---|---|---|
type 不是 "private" | 400 | invalid_request |
| participants 长度不为 1 | 400 | invalid_request |
| 不能和自己创建私聊 | 400 | invalid_request |
| 对方用户不存在 | 404 | not_found |
2. 获取会话列表
GET /romp/v1/conversations获取当前用户参与的所有会话,按最近活跃时间降序排列。
查询参数:
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
limit | number | 否 | 20 | 每页数量,最大 50 |
cursor | string | 否 | -- | 上一页最后一条的 id |
响应 200 OK:
{
"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].kind | summary |
|---|---|
text | 文本内容(超过 50 字截断并加 ...) |
thinking | [思考中...] |
image | [图片] |
audio | [语音] |
video | [视频] |
file | [文件] 文件名 |
| 其他 | [消息] |
若消息包含多个 part,summary 仅反映第一个 part 的内容。混合消息(如图文混排)的摘要以首个 part 为准。
排序规则:
会话列表按以下优先级排序:
- 置顶会话排在最前(按
pinned_at降序) - 非置顶会话按
updated_at降序
新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
is_pinned | boolean | 当前用户是否置顶了该会话 |
pinned_at | string | null | 置顶时间(ISO 8601),未置顶为 null |
3. 置顶会话
POST /romp/v1/conversations/:id/pin将指定会话置顶。每个用户独立管理置顶状态,不影响其他参与者。
路径参数:
| 参数 | 说明 |
|---|---|
id | 会话 UUID |
响应 200 OK:
{
"ok": true,
"pinned_at": "2026-03-12T10:00:00.000Z"
}错误码:
| 场景 | HTTP | code |
|---|---|---|
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
4. 取消置顶
DELETE /romp/v1/conversations/:id/pin取消指定会话的置顶状态。
路径参数:
| 参数 | 说明 |
|---|---|
id | 会话 UUID |
响应 200 OK:
{ "ok": true }错误码:
| 场景 | HTTP | code |
|---|---|---|
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
5. 标记已读
POST /romp/v1/conversations/:id/read将指定会话标记为已读(到某条消息为止)。只能向前推进,不会倒退。
路径参数:
| 参数 | 说明 |
|---|---|
id | 会话 UUID |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
message_id | string | 是 | 已读到的最新消息 ID |
响应 200 OK:
{
"last_read_message_id": "msg-uuid"
}行为说明:
- 如果
message_id比当前已读位置旧,不会倒退,返回当前值 - 如果
message_id更新,向前推进到该消息 - 原子操作,行级锁保证并发安全
错误码:
| 场景 | HTTP | code |
|---|---|---|
| message_id 缺失 | 400 | invalid_request |
| 消息不存在或不属于此会话 | 400 | invalid_request |
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
6. 发送输入状态
POST /romp/v1/conversations/:id/typing向指定会话广播当前用户的输入状态。通过 Supabase Realtime Broadcast 推送给会话中的其他参与者。
路径参数:
| 参数 | 说明 |
|---|---|
id | 会话 UUID |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
status | string | 是 | 输入状态枚举值 |
状态枚举:
| 值 | 含义 |
|---|---|
thinking | AI 正在思考 |
typing | 用户/AI 正在输入 |
tool_calling | AI 正在调用工具 |
idle | 空闲(停止输入) |
服务端节流: 同一用户在同一会话中发送相同状态,1 秒内仅广播一次。重复请求不会报错,返回 { "ok": true } 但不触发广播。
响应 200 OK:
{ "ok": true }Broadcast 事件格式(Realtime 推送):
{
"event": "typing",
"payload": {
"user_id": "user-a-uuid",
"status": "typing",
"user_name": "张三"
}
}频道名:romp-typing:<conversation_id>
错误码:
| 场景 | HTTP | code |
|---|---|---|
| status 缺失或无效 | 400 | invalid_request |
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
消息端点
7. 发送消息
POST /romp/v1/messages向指定会话发送一条消息。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id | string | 是 | 目标会话 ID |
parts | Part[] | 是 | 消息内容,至少 1 个 Part |
reply_to | string | 否 | 回复的消息 ID |
metadata | object | 否 | 自定义元数据,透传存储 |
{
"conversation_id": "conv-uuid",
"parts": [
{ "kind": "text", "text": "你好" }
],
"reply_to": null,
"metadata": {
"client_msg_id": "local-uuid"
}
}Part 类型:
| kind | 必填字段 | 示例 |
|---|---|---|
text | text: string(非空) | {"kind":"text","text":"你好"} |
thinking | content: string(非空) | {"kind":"thinking","content":"让我想想..."} |
image | file_id: string | {"kind":"image","file_id":"uuid","url":"...","name":"photo.jpg"} |
audio | file_id: string | {"kind":"audio","file_id":"uuid","url":"...","duration":5.2} |
video | file_id: string | {"kind":"video","file_id":"uuid","url":"..."} |
file | file_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:
{
"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
错误码:
| 场景 | HTTP | code |
|---|---|---|
| conversation_id 缺失 | 400 | invalid_request |
| parts 为空或格式错误 | 400 | invalid_request |
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
| reply_to 消息不存在或不属于此会话 | 400 | invalid_request |
8. 获取消息历史
GET /romp/v1/messages获取指定会话的消息历史,支持双向游标分页。
查询参数:
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
conversation_id | string | 是 | -- | 会话 ID |
limit | number | 否 | 50 | 每页数量,最大 100 |
before | string | 否 | -- | 获取此消息之前的(更早的) |
after | string | 否 | -- | 获取此消息之后的(更新的) |
before和after互斥,不可同时使用。
响应 200 OK:
{
"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> | 时间正序 | 下滑加载更新消息 |
错误码:
| 场景 | HTTP | code |
|---|---|---|
| conversation_id 缺失 | 400 | invalid_request |
| before 和 after 同时传 | 400 | invalid_request |
| 游标消息不存在 | 400 | invalid_request |
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
9. 编辑消息
PATCH /romp/v1/messages/:id编辑一条已发送的消息。仅允许修改文本类 parts(text、thinking 等),不允许编辑包含文件附件(image/audio/video/file)的 parts。
路径参数:
| 参数 | 说明 |
|---|---|
id | 消息 UUID |
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
parts | Part[] | 是 | 新的消息内容,至少 1 个 Part,仅允许文本类 |
{
"parts": [
{ "kind": "text", "text": "修改后的内容" }
]
}响应 200 OK:
{
"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/filekind - 编辑不会触发推送通知
错误码:
| 场景 | HTTP | code |
|---|---|---|
| parts 为空或格式错误 | 400 | invalid_request |
| parts 包含文件类 kind | 400 | invalid_request |
| 消息不存在 | 404 | not_found |
| 非消息发送者 | 403 | forbidden |
| 不再是会话参与者 | 403 | forbidden |
推送端点
10. 注册推送 Token
POST /romp/v1/push/register注册设备推送 Token,用于 App 端接收原生推送通知。仅 JWT 认证。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
token | string | 是 | 推送 token |
platform | string | 是 | 移动端:"ios" 或 "android";Web Push:"web" |
provider | string | 否 | 推送通道(默认 "fcm"),可选值:fcm、apns、expo、jpush、emas、web-push |
integrationIdentifier | string | 否 | Novu FCM 集成标识,仅 provider="fcm" 时有意义 |
subscriptionKeys | object | 否 | Web Push 订阅密钥(仅 provider="web-push" 时必填) |
subscriptionKeys 结构(Web Push):
| 字段 | 类型 | 说明 |
|---|---|---|
p256dh | string | P-256 ECDH 公钥 |
auth | string | 认证密钥 |
provider-platform 组合约束:
| provider | 允许的 platform | 说明 |
|---|---|---|
fcm | ios, android | 通用 |
apns | ios, android | 通用 |
expo | ios, android | 通用 |
jpush | android | 仅 Android |
emas | ios, android | 阿里云 EMAS |
web-push | web | 浏览器推送 |
行为说明:
- 幂等:同一 token 重复注册仅更新
updated_at - Token 迁移:若 token 已属于其他用户,原子迁移到当前用户
- Novu 同步仅在
provider="fcm"时执行
响应 200 OK:
{ "success": true }错误码:
| 场景 | HTTP | code |
|---|---|---|
| 非 JWT 认证 | 403 | forbidden |
| token 缺失 | 400 | invalid_request |
| platform 无效 | 400 | invalid_request |
| provider 无效 | 400 | invalid_request |
| web-push 缺少 subscriptionKeys | 400 | invalid_request |
| web-push platform 非 "web" | 400 | invalid_request |
11. 注销推送 Token
POST /romp/v1/push/unregister
DELETE /romp/v1/push/register注销设备推送 Token。仅 JWT 认证,幂等。
支持两种方式(DELETE 为兼容别名,部分客户端/网关对 DELETE body 支持较差)。
POST 请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
token | string | 是 | 要注销的推送 token |
DELETE 请求: body 或 ?token=xxx query param
响应 200 OK:
{ "success": true }错误码:
| 场景 | HTTP | code |
|---|---|---|
| 非 JWT 认证 | 403 | forbidden |
| token 缺失 | 400 | invalid_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:
{ "subscriberHash": "hex-hmac-sha256-string" }错误码:
| 场景 | HTTP | code |
|---|---|---|
| 非 JWT 认证 | 403 | forbidden |
| Novu 未配置 | 503 | internal_error |
文件端点
13. 上传文件
POST /romp/v1/upload
Content-Type: multipart/form-data上传多媒体文件,用于在消息中发送图片、语音、附件等。仅 JWT 认证。
请求参数(form-data):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | File | 是 | 文件 |
conversation_id | string | 是 | 目标会话 ID |
purpose | string | 否 | message_image / message_audio / message_file |
文件大小限制:
| purpose | 限制 | 允许的 MIME 类型 |
|---|---|---|
message_image | 20 MB | image/jpeg, image/png, image/gif, image/webp |
message_audio | 5 MB | audio/mp4, audio/aac, audio/mpeg, audio/webm, audio/ogg, audio/wav, audio/m4a, audio/x-m4a |
message_file | 50 MB | 不限制 |
| 未指定 | 50 MB | 不限制 |
MIME 规范化:上传时后端会自动规范化 MIME 类型——去除参数部分并转为小写。例如浏览器 MediaRecorder 输出的
audio/webm;codecs=opus会被规范化为audio/webm后再做白名单匹配。
响应 200 OK:
{
"file_id": "uuid",
"name": "photo.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"url": "签名 URL(有效期 1 小时)",
"expires_at": "2026-03-09T01:00:00.000Z"
}错误码:
| 场景 | HTTP | code |
|---|---|---|
| 非 JWT 认证 | 403 | forbidden |
| 未上传文件 | 400 | invalid_request |
| conversation_id 缺失 | 400 | invalid_request |
| 文件超过大小限制 | 400 | file_too_large |
| MIME 类型不允许 | 400 | invalid_file_type |
| 会话不存在 | 404 | not_found |
| 不是会话参与者 | 403 | forbidden |
14. 刷新文件 URL
GET /romp/v1/files/:fileId/url获取文件的新签名 URL(有效期 1 小时)。用于文件 URL 过期后重新获取可访问的下载地址。
路径参数:
| 参数 | 说明 |
|---|---|
fileId | 文件 UUID |
响应 200 OK:
{
"url": "新签名 URL",
"expires_at": "2026-03-09T01:00:00.000Z"
}错误码:
| 场景 | HTTP | code |
|---|---|---|
| 文件不存在 | 404 | not_found |
| 无权访问该文件 | 403 | forbidden |
15. 获取文件预览
GET /romp/v1/files/:fileId/preview获取文件的缩略图预览 URL。适用于图片类文件的列表展示场景。
路径参数:
| 参数 | 说明 |
|---|---|
fileId | 文件 UUID |
响应 200 OK:
{
"url": "优先缩略图,无则原图",
"original_url": "原图 URL",
"thumbnail_url": "缩略图 URL 或 null",
"expires_at": "2026-03-09T01:00:00.000Z"
}错误码:
| 场景 | HTTP | code |
|---|---|---|
| 文件不存在 | 404 | not_found |
| 无权访问该文件 | 403 | forbidden |
数据模型
ProfileType
type ProfileType = 'user' | 'agent' | 'system'Participant 对象
interface Participant {
id: string // user_id
type: ProfileType // 用户类型(user/agent/system)
name: string | null
avatar: string | null
role: 'owner' | 'member'
}Sender / Peer 对象
interface Sender {
id: string // user_id
type: ProfileType // 用户类型(user/agent/system)
name: string | null
avatar: string | null
}Message 对象
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 类型
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 对象
interface LastMessage {
id: string
sender_name: string
summary: string // 摘要文本
created_at: string
}相关文档
| 文档 | 说明 |
|---|---|
| ROMP API Reference(内部文档) | 完整参考文档 |
| 实时消息订阅(内部文档) | Supabase Realtime 订阅方式 |
| App 推送集成指南(内部文档) | iOS/Android/Flutter 推送接入指南 |