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、webhooks.ts——用户 Webhook 注册 CRUD;webhook-trigger.ts——内部投递工具函数,供 Agent Webhook / User Webhook 触发时调用,自身不注册路由)

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


认证

所有端点需要 Authorization 头:

Authorization: Bearer <TOKEN>

支持四种认证方式:

方式Token 格式origin 值适用场景
JWT(用户登录态)Supabase JWTclient前端/App 用户操作
API Keywn- 前缀密钥api服务端/机器人调用
Agent Tokenwna- 前缀永久密钥apiAI Agent 在会话中收发消息
OAuthwno- 前缀 access tokenclient移动端 mobile:full scope 业务接口

Agent Token 是永久有效的 opaque token,创建 Agent 时签发(明文仅展示一次),数据库仅存 SHA-256 哈希。允许访问 ROMP 消息收发和文档操作端点。

Agent 还可选择内置 UI Workflow:系统把会话文本转换为 HTML UI Create/Update,并用标准 text + webpage parts 在原会话回复,不新增 UI 专属 ROMP 消息类型。

移动端 OAuth token 仅能访问 mobile:full scope 白名单内的业务接口。


错误格式

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服务端错误
service_unavailable503ROMP 服务不可用(站点总开关 site_settings.romp.enabled 关闭,或检查失败)

用户级禁用:即使站点开关开启,若当前用户被打上 romp:disabled 权限标记(管理后台可配置),其所有 ROMP 请求也会被拦截,返回 403 forbidden(与"非会话参与者"共用同一 code,但语义是账号级全局禁用而非单会话权限)。


会话端点

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 生成;当首个 part 非 text / thinking 时,会附加文本类 part 预览:

parts[0].kindsummary
text文本内容(超过 50 字截断并加 ...
thinking[思考中...]
image[图片]
audio[语音]
video[视频]
file[文件] 文件名
tool_call[工具] 工具名
tool_resultis_error=false[工具结果]
tool_resultis_error=true[工具失败]
webpage(有 title[网页] {title}
webpage(无 title[链接] {url}
其他[消息]

若消息包含多个 part 且首个 part 是 text / thinking,summary 仅反映第一个 part 的内容;首个 part 是非文本类型(如 image / tool_call)时,会自动拼接后续 text part 预览(例如 [图片] 文字内容[工具] search_web 搜索完成)。

排序规则:

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

  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

向指定会话广播当前用户的输入状态。通过自建 WebSocket 网关(/ws/romp)广播给所有已订阅该会话的连接(broadcastToConversation 不排除发送者自己),不经过 Supabase Realtime。发送者自己的连接也会收到这条 typing 事件,需要客户端自行按 user_id 过滤掉自己(不要在服务端假设已经排除)。

路径参数:

参数说明
id会话 UUID

请求体:

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

状态枚举:

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

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

响应 200 OK

json
{ "ok": true }

WebSocket 推送格式(/ws/romp):

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

仅推送给已对该 conversation_id 发送过 subscribe 指令的连接,无 Realtime channel 概念。完整协议见 docs/romp/realtime.md

错误码:

场景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":"让我想想..."}
image无强制(见下方说明){"kind":"image","file_id":"uuid","url":"...","name":"photo.jpg"}
audio无强制(见下方说明){"kind":"audio","file_id":"uuid","url":"...","duration":5.2,"transcript":"语音转写文本","role":"source_audio"}
video无强制(见下方说明;前端暂无渲染实现,仅后端透传){"kind":"video","file_id":"uuid","url":"..."}
file无强制(见下方说明){"kind":"file","file_id":"uuid","url":"...","name":"report.pdf","size":102400}
webpageurl: string(非空)、title: string(非空){"kind":"webpage","url":"https://...","title":"页面标题","description":"...","icon":"...","screenshot":"...","context":{"document_id":"..."}}
tool_calltool_call_id: stringname: stringstatus: 'pending'|'running'|'done'|'error'{"kind":"tool_call","tool_call_id":"call_1","name":"search_web","arguments":{"q":"..."},"status":"running"}
tool_resulttool_call_id: string{"kind":"tool_result","tool_call_id":"call_1","result":"...","is_error":false,"duration_ms":1234}
其他 kind无强制校验直接存储(前向兼容)

多媒体 part(image/audio/video/file)中的 url 为签名 URL,有效期 1 小时。过期后可通过「刷新文件 URL」端点获取新 URL。image/audio/video/file 的 part 结构本身不强制要求 file_id——validateParts 的字段级校验不覆盖这四种 kind(不同于 text/thinking/webpage/tool_call/tool_result),缺失 file_id 的 part 能通过结构校验。但 POST /v1/messages 在结构校验之后还有一段独立的文件绑定逻辑:只要 part 携带了非空 file_id,就会校验其存在性/归属/所属会话/绑定状态(对应下方错误码表的 file_id 不存在 等四行),校验失败直接 400/403,不会静默忽略。

audio.transcript / audio.role(2026-06-29 起):客户端可携带 transcript(语音转写文本);role 由服务端在 Agent Webhook 归一化时标记为 source_audio(若客户端已设置则保留原值)。详见下方「Agent Webhook 音频转写归一化」。

webpage 用于分享网页卡片 / AgentSidebar 自动携带的页面上下文,驱动"纯网页消息静默不推送"等业务逻辑;icon/context 字段主要服务于 AgentSidebar 自动上下文场景,不属于网页卡片协议本体。

工具调用 Part(tool_call / tool_result:用于 Agent 在工作流中向用户展示工具调用过程。

  • tool_call.tool_call_id / name 长度上限 256 字符
  • tool_call.arguments 必须是 object 或 string;JSON 序列化后 ≤ 64KB
  • tool_call.status 严格枚举:pending / running / done / error
  • tool_result.is_error 必须是 boolean;duration_ms 必须是非负有限数;result JSON 序列化后 ≤ 64KB
  • 单个 tool_call / tool_result part 整体 JSON 序列化 ≤ 128KB
  • 这两类 part 允许通过 PATCH /v1/messages/:id 编辑(与 text / thinking 同列),用于 Agent 流式推进工具状态

metadata.client_msg_id(推荐):

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

metadata 系统保留字段input_modality / source_modality / primary_kind 由服务端在构建 Agent Webhook payload 时自动注入(语音转写归一化场景)——这个自动生成的值只存在于投递给 Agent 的 payload 里,不会回写数据库。但 normalizeMessageMetadata() 对客户端传入的 metadata 对象不做任何字段过滤,如果客户端自己在 metadata 里传了这三个同名字段,会被原样存库并出现在本端点的响应里——请视为保留名,客户端发送时应避免使用这三个字段名,防止和服务端自动生成的语义冲突。详见「Agent Webhook 音频转写归一化」(内部文档 docs/romp/api-reference.md)。

响应 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 或 OAuth(mobile:full)认证发送(用户在 App 中操作)
api通过 API Key 或 Agent Token 发送(服务端/机器人/AI Agent)
system系统自动发送的消息

副作用:

  • 会话的 updated_at 自动更新
  • 异步触发推送通知(发送者账号以外的所有参与者,按账号级排除,即排除发送者本人全部设备):Novu(In-App + FCM)、JPush、EMAS、Web Push
  • 新消息通过自建 WebSocket 网关(/ws/romp)推送:会话内所有参与者的 user-level stream 自动收到 message.newromp_messages 表虽然仍带 REPLICA IDENTITY FULL 并在 Realtime 发布中,具备被 Supabase Realtime 订阅的能力,但前端 stores/romp.ts 当前没有建立实际的 postgres_changes 订阅(ensureRealtimeSubscription() 现在是空实现,注释明确写"Supabase Realtime 已移除,消息完全通过 ROMP WebSocket 接收")——消息实时性目前依赖 WebSocket 单通道,不要按"双通道"设计
  • 本次请求不是通过 Agent Token 认证auth.method !== 'agent',判断的是认证方式而非发送者 profiles.type)时,向会话中除发送者本人外的 Agent 类型参与者异步触发 Agent Webhook(两层过滤共同避免 Agent 自触发循环)

错误码:

场景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

编辑一条已发送的消息。禁止编辑文件附件类 partsimage / audio / video / file);其他 kind 都可以编辑(含 textthinkingtool_calltool_result 以及自定义 kind)。tool_call / tool_result 在 Agent 流式推进工具状态时尤其常用。

路径参数:

参数说明
id消息 UUID

请求体:

字段类型必填说明
partsPart[]新的消息内容,至少 1 个 Part;不允许包含文件类(image/audio/video/file
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
  • 编辑不会触发推送通知(Novu/JPush/EMAS/Web Push),但会通过自建 WebSocket 网关向已订阅该会话的连接广播 message.update 事件(Agent 流式更新依赖此机制)

错误码:

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

推送端点

10. 注册推送 Token

POST /romp/v1/push/register

注册设备推送 Token,用于 App 端接收原生推送通知。支持 JWT / OAuth (mobile:full) 认证。

请求体:

字段类型必填说明
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通用
jpushios, android国内全平台(iOS 双发 APNs prod+dev,Android 单发)
emasios, android阿里云 EMAS
web-pushweb浏览器推送

行为说明:

  • 幂等 = 以 token 为唯一键复用同一行:不是"仅更新 updated_at",重复注册会用本次携带的值覆盖 user_id/provider/platform 等归属与配置字段
  • Token 迁移:移动端(ios/android)若 token 已属于其他用户,原子迁移到当前用户,仅 provider="fcm"/"jpush" 时才双向同步新旧用户的 Novu credentials;web-push 走单纯 upsert,不查询、不同步旧归属用户,是静默覆盖
  • Novu 同步(fire-and-forget)在 provider="fcm" 时无条件执行;provider="jpush"额外受环境变量 JPUSH_NOVU_SYNC_ENABLED 门控,只有精确等于 'true' 才会同步;web-push 受另一个灰度开关 WEBPUSH_NOVU_SYNC_ENABLED 控制;apns/expo/emas 目前不触发 Novu 同步。注意:apns/expo 虽在校验白名单内可注册成功,但推送发送逻辑未实现对应通道,用这两个 provider 注册的 token 目前不会实际收到任何推送。
  • JPush 有两种互斥的投递模式,由 JPUSH_NOVU_SYNC_ENABLED 决定:未设置或非 'true'(默认)时走直连模式——消息发送时 hasJPush=true,直接调用 JPush API 推送,不经过 Novu;设为 'true' 时切换为 Novu 托管模式——消息发送时 hasJPush 被强制置 false(跳过直连),JPush 推送完全依赖本端点同步进 Novu 的 push-webhook credentials 由 Novu 侧转发,若 Novu 的 webhook 集成没配置好,JPush 用户会收不到任何推送。两种模式二选一,不是"直连 + 顺带同步"的叠加关系。

响应 200 OK

json
{ "success": true }

错误码:

场景HTTPcode
非 JWT / OAuth 认证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 / OAuth (mobile:full) 认证,幂等。

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

POST 请求体:

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

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

响应 200 OK

json
{ "success": true }

错误码:

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

Novu 认证端点

12. 获取 Novu subscriberHash

GET /romp/v1/novu/subscriber-hash

返回当前用户的 HMAC subscriberHash,供 Web/App 端 Novu SDK 初始化时传入,启用安全校验。支持 JWT / OAuth (mobile:full) 认证。

HMAC = SHA256(subscriberId, NOVU_API_KEY)

响应 200 OK

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

错误码:

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

文件端点

13. 上传文件

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

上传多媒体文件,用于在消息中发送图片、语音、附件等。支持 JWT / OAuth (mobile:full) 认证。

请求参数(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 / OAuth 认证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 过期后重新获取可访问的下载地址。

支持 JWT / OAuth (mobile:full) 认证。

路径参数:

参数说明
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。适用于图片类文件的列表展示场景。

路由本身不限制 auth.method,但 API Key / Agent Token 需要后台 permission_groups 显式授权覆盖该路径才能通过认证中间件——当前默认权限分组未覆盖此路径,实际只有 JWT 能访问;不支持 OAuth(mobile:full——与「刷新文件 URL」端点不同,该端点未列入 OAuth scope 白名单,移动端 OAuth token 调用会被拦截。返回的 codeforbidden(ROMP 路由统一把 401/403 的字符串 error 包装成 {code, message}),原始的 insufficient_scope 只会出现在 message 字段里,不是 code 本身。

路径参数:

参数说明
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; source?: 'audio_transcript' | 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[]; transcript?: string; role?: 'source_audio' | string }
  | { 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: 'webpage'; url: string; title: string; description?: string; icon?: string; screenshot?: string; context?: { document_id?: string; team_slug?: string; project_id?: string; collection_id?: string } }
  | { kind: 'tool_call'; tool_call_id: string; name: string; arguments?: Record<string, unknown> | string; status: 'pending' | 'running' | 'done' | 'error' }
  | { kind: 'tool_result'; tool_call_id: string; result?: unknown; is_error?: boolean; duration_ms?: number }
  | { kind: string; [key: string]: unknown }  // 前向兼容

LastMessage 对象

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

相关文档

文档说明
ROMP API Reference(内部文档)完整参考文档
实时消息订阅(内部文档)ROMP WebSocket 接入指南(消息实时性目前是 WS 单通道,Realtime 订阅代码已停用)
App 推送集成指南(内部文档)iOS/Android/Flutter 推送接入指南

补充端点

本节补充 plans/need-update-api.md 中尚未覆盖到 docs-site 的接口。端点标题保持严格的 METHOD /path 格式,便于后台文档覆盖率服务识别。

GET /romp/v1/push/diagnostics

读取 ROMP 推送链路诊断信息(渠道配置状态、token 分布、近 24 小时推送量)。

认证:仅 JWT,且额外要求 site:admin 权限(grantspermission_code='site:admin'),非站点管理员返回 403 forbidden请求:无请求体,无查询参数。

响应 200 OK

json
{
  "channels": {
    "novu": { "enabled": true },
    "jpush": { "enabled": true },
    "emas": { "enabled": false },
    "web-push": { "enabled": true }
  },
  "tokenDistribution": [
    { "provider": "fcm", "platform": "ios", "count": 80 },
    { "provider": "jpush", "platform": "android", "count": 30 }
  ],
  "last24h": { "total": 0, "byChannel": {} }
}

channels 是否 enabled 分别取决于:novu(是否配置了 NOVU_API_KEY+NOVU_API_URL,FCM 走这条通道)、jpush/emas/web-push(各自的配置检测函数)。tokenDistribution 是按 (provider, platform) 分组的计数数组,非固定 key 的对象。

已知问题last24h 的读取/聚合逻辑本身正常,但写入侧是死代码——romp_push_metrics 表的 recordPushMetric() 从未被任何推送发送路径实际调用,正常业务流程下这张表没有数据,接口通常返回 {total:0, byChannel:{}}(不是接口保证恒为 0,只是缺写入者,若表里有其他来源的数据会被正常聚合出来)。total 统计的是命中的指标行数,不是 sent+failed 的总量。

常见错误401 未认证,403site:admin;服务端异常返回 500


GET /romp/v1/webhooks

列出当前用户注册的 Webhook。

认证:路由本身不限制 auth.method,但 API Key / Agent Token 需要后台 permission_groups 显式授权覆盖该路径——当前默认权限分组未覆盖 /romp/v1/webhooks*,实际只有 JWT 能访问;不支持 OAuth(mobile:full)。 请求:无请求体,无查询参数,不支持分页——一次性返回当前用户全部 webhook。

响应 200 OK(裸数组,无分页包裹):

json
[
  { "id": "uuid", "user_id": "uuid", "url": "https://...", "events": ["message.new"], "conversations": null, "created_at": "2026-..." }
]

常见错误401 未认证;服务端异常返回 500


POST /romp/v1/webhooks

注册一个用户级 Webhook。这是离线补偿通道,不是"注册后必收到全部匹配事件":只有当消息接收方(会话内除发送者外的其他参与者)在投递那一刻没有 ROMP WebSocket 在线连接时,才会查询其名下 active=true 的 webhook 并投递;发送者本人永远被排除,在线用户即使注册了 webhook 也不会收到(在线时消息走 WS 实时推送)。conversations/events 只是在"确实要投递给这个离线用户"之后做的二次范围过滤,不传不代表在线时也会收到。纯 webpage 静默上下文消息同样不会触发 User Webhook。

认证:路由本身不限制 auth.method,但 API Key / Agent Token 需要后台 permission_groups 显式授权覆盖该路径——当前默认权限分组未覆盖 /romp/v1/webhooks*,实际只有 JWT 能访问;不支持 OAuth(mobile:full)。

请求体:

字段类型必填说明
urlstring必须是合法 HTTPS URL;创建时只做字符串层校验(拒绝 localhost/字面量内网 IP/回环地址),不做 DNS 解析
secretstring用于签名投递请求
eventsstring[]目前仅支持 ["message.new"],传其他值返回 400
conversationsstring[]限定只在自己离线时推送这些会话的事件;不传则不做会话范围过滤
team_idstring若传入,服务端校验当前用户是该团队的 accepted 成员;不传则从认证上下文(API Key/Agent 场景)取团队——当前默认权限分组下这两种 token 实际访问不到本端点,该分支暂时用不到

响应 201 Created 返回插入后的完整 webhook 行(含 id/user_id/team_id/url/secret/events/conversations/created_at)。

常见错误400 url 缺失/非法 HTTPS/hostname 字面量是内网 IP/events 含不支持值/conversations 非字符串数组,403 非该团队成员,500 服务端异常。

注册时的 SSRF 校验不是完整防护:用域名(而非 IP 字面量)指向内网地址会在创建时通过校验、正常拿到 201。真正的 DNS 解析 + 私网 IP 检测发生在投递那一刻,不是注册时——这类 webhook 能注册成功,但永远收不到任何投递,且不会有任何报错回传。


DELETE /romp/v1/webhooks/:id

删除指定 Webhook(仅能删除自己名下的)。

认证:路由本身不限制 auth.method,但 API Key / Agent Token 需要后台 permission_groups 显式授权覆盖该路径——当前默认权限分组未覆盖 /romp/v1/webhooks*,实际只有 JWT 能访问;不支持 OAuth(mobile:full)。 路径参数

参数说明
idWebhook UUID

请求:无请求体。

响应:成功返回 204 No Content(空 body,非 JSON)

常见错误404 该 webhook 不存在或不属于当前用户,500 服务端异常。

AI Workflow Editor