45 KiB
IM WebSocket 前后端接口文档
本文档根据
/Users/superysh/im/prototype下的产品原型图整理,用于移动端/前端与 Tinode 后端对接。协议风格遵循 Tinodedocs/API.md:默认全部业务通信通过 WebSocket JSON 文本帧完成;大文件仍沿用 Tinode 现有 HTTP 上传/下载端点,上传完成后通过 WebSocket 发送附件引用。
目录
适用范围
原型覆盖页面
原型图覆盖以下业务域:
| 业务域 | 页面示例 |
|---|---|
| 登录注册 | 登录、注册、忘记密码、验证码、实名认证、身份验证、独立密码 |
| 消息 | 消息列表、私聊、群聊、文件助手、系统消息、长按菜单、多选、转发、撤回、删除 |
| 联系人 | 朋友、新朋友、添加好友、好友主页、备注、拉黑、黑名单、删除好友 |
| 群聊 | 创建群聊、加入群聊、群二维码、群资料、群公告、群文件、群管理员、禁言、群主转让、进群验证 |
| 文件与收藏 | 文件发送、下载、打开、群文件、我的收藏、收藏分享 |
| 通话 | 语音通话、视频通话、等待接听、通话中、关闭摄像头/麦克风/扬声器、悬浮状态 |
| 话题 | 话题列表、发布话题、话题详情、话题人数、话题类型、积分不足 |
| 设置 | 个人信息、个性签名、隐私设置、青少年模式、版本更新、清空缓存、意见反馈 |
| 安全 | 反诈弹窗、用户协议、推送权限、相机/麦克风权限、安全提醒、举报 |
与 Tinode 原生能力的关系
| 能力 | 接入方式 |
|---|---|
| 登录、注册、Token、验证码 | 使用 {hi}、{acc}、{login},必要时接入 code/rest auth |
| 私聊、群聊、频道 | 使用 {sub}、{leave}、{pub}、{get}、{set}、{del} |
| 消息回执、输入中、语音录制中 | 使用 {note},服务端转发为 {info} |
| 用户资料、群资料、备注、置顶、免打扰 | 使用 public、private、aux |
| 文件、图片、语音、视频 | 大文件先上传 /v0/file/u/,再通过 {pub} 发送 Drafty 附件 |
| 群权限、管理员、禁言、踢人 | 使用 Tinode ACL 与 {set sub}、{del what="sub"},业务状态写入 aux |
| 收藏、话题、签到、青少年模式、反诈提示 | 使用本产品扩展命名空间 x-im-* |
协议总则
WebSocket 端点
客户端统一连接:
ws://{host}/v0/channels?apikey={api_key}
wss://{host}/v0/channels?apikey={api_key}
每个 WebSocket 文本帧只发送一个 JSON 消息。二进制帧不用于业务数据。
首包与登录顺序
客户端连接成功后必须先发送 {hi},再发送 {login} 或 {acc}。登录后建议立即订阅 me 和 fnd:
{"hi":{"id":"100","ver":"0.25","ua":"IM/1.0 (iOS)","platf":"ios","lang":"zh-CN","tenant":"acme"}}
{"login":{"id":"101","scheme":"basic","secret":"MTg4MjY4Mjc3NTQ6MTIzNDU2"}}
{"sub":{"id":"102","topic":"me","get":{"what":"desc sub tags cred"}}}
{"sub":{"id":"103","topic":"fnd"}}
消息类型
客户端到服务端:
| 包 | 用途 |
|---|---|
{hi} |
握手、声明企业码、客户端版本和设备信息 |
{acc} |
注册、修改认证参数、提交验证码 |
{login} |
登录、Token 登录、重置密码流程 |
{sub} |
订阅/进入会话、创建群、创建私聊、拉取初始数据 |
{leave} |
离开会话或退订会话 |
{pub} |
发送持久化消息 |
{get} |
查询资料、订阅、消息历史、删除记录、辅助数据 |
{set} |
更新资料、备注、权限、群设置、辅助数据 |
{del} |
删除消息、订阅、topic、凭证 |
{note} |
发送临时通知,如已读、已收、输入中、通话信令 |
服务端到客户端:
| 包 | 用途 |
|---|---|
{ctrl} |
请求结果,应答成功或失败 |
{data} |
持久化消息投递 |
{meta} |
资料、订阅、标签、凭证、删除记录、辅助数据返回 |
{pres} |
在线、离线、权限、资料更新、消息到达等通知 |
{info} |
由 {note} 转发而来的临时通知 |
请求 ID 规范
客户端必须为需要应答的请求设置 id。建议格式:
{module}-{timestamp_ms}-{seq}
示例:
{"get":{"id":"msg-1722400000000-1","topic":"grpAbC123","what":"data","data":{"limit":20}}}
扩展命名空间
Tinode 原生字段保持不变。本产品扩展字段统一使用 x-im-*:
| 位置 | 示例 | 说明 |
|---|---|---|
head.x-im-type |
"text"、"card"、"topic-share" |
消息业务类型 |
head.x-im-client-mid |
"local-uuid" |
客户端本地消息 ID,用于发送中/失败重试 |
head.x-im-anti-fraud |
true |
需要客户端展示反诈提醒 |
content.x-im-* |
{...} |
消息内容里的业务负载 |
public.x-im-* |
{...} |
对所有可见的业务资料 |
private.x-im-* |
{...} |
仅当前用户可见的配置 |
aux.x-im-* |
{...} |
群/频道管理员可写、成员可读的配置 |
第一阶段服务端已启用轻量校验:
| 字段 | 规则 |
|---|---|
head.x-im-type |
必须是文档附录里的已知消息类型 |
private.x-im-applyText |
字符串,最长 256 个字符 |
private.x-im-applyStatus、private.x-im-joinStatus |
枚举:pending、accepted、rejected、expired |
private.x-im-joinApplyText |
字符串,最长 256 个字符 |
private.x-im-blocked、private.x-im-muted、private.x-im-pinned |
布尔值 |
aux.x-im-group.joinApproval、onlyAdminInvite、allowMemberFileUpload、muteAll |
布尔值 |
aux.x-im-group.mutedUsers |
Tinode 用户 ID 数组 |
aux.x-im-group.announcement.text |
字符串,最长 2000 个字符 |
公共数据约定
时间与 ID
| 字段 | 说明 |
|---|---|
ts、created、updated |
RFC3339 UTC 字符串,如 "2026-07-31T08:00:00.000Z" |
user |
Tinode 用户 ID,如 usrAbCdEf123 |
topic |
Tinode topic,如 usrAbCdEf123、grpAbCdEf123、chnAbCdEf123 |
seq |
topic 内递增消息序号 |
clear |
删除事务序号 |
用户公开资料 public
用户资料页面、好友主页、群成员头像昵称均使用用户 public。
{
"fn": "张三",
"photo": {
"ref": "/v0/file/s/avatar.jpg",
"type": "image/jpeg"
},
"note": "成人最懂什么都没留下",
"x-im-gender": "female",
"x-im-birthday": "2008-02-07",
"x-im-area": "广东省广州市",
"x-im-id-no": "84444646554"
}
用户私有资料 private
仅当前用户可见。
{
"comment": "好友备注",
"tpins": ["usrPeer001", "grpTeam001"],
"x-im-settings": {
"teenMode": false,
"independentPassword": true,
"allowSearchByPhone": true,
"allowSearchByQr": true,
"allowSearchByGroup": true,
"pushEnabled": true
}
}
群公开资料 public
群资料、群二维码、群设置页读取群 topic 的 public。
{
"fn": "新人交流群",
"photo": {
"ref": "/v0/file/s/group-avatar.jpg",
"type": "image/jpeg"
},
"note": "群主很懒,还没有群简介",
"x-im-ownerName": "王子民",
"x-im-qrcode": "im://group/grpAbCdEf123"
}
群业务配置 aux
群设置、群公告、群文件权限、进群验证、禁言列表使用 aux。
{
"x-im-group": {
"joinApproval": true,
"onlyAdminInvite": false,
"onlyAdminEditInfo": true,
"allowMemberFileUpload": true,
"muteAll": false,
"mutedUsers": ["usrMuted001"],
"announcement": {
"text": "为帮助新用户熟悉群规则,请勿发布违规内容。",
"updatedBy": "usrOwner001",
"updatedAt": "2026-07-31T08:00:00.000Z"
}
}
}
当前用户对会话的私有设置 desc.private
聊天设置页里的备注、免打扰、置顶、拉黑、本地草稿等使用当前用户对 topic 的 private。
{
"comment": "王子民",
"arch": false,
"x-im-muted": false,
"x-im-pinned": true,
"x-im-blocked": false,
"x-im-draft": "稍后回复"
}
连接与认证
登录
原型页面:登录页面、注册页面。
客户端先发送 {hi},再使用手机号/用户名 + 密码登录。secret 为 base64(username + ":" + password)。
{"login":{"id":"login-1","scheme":"basic","secret":"MTg4MjY4Mjc3NTQ6MTIzNDU2"}}
成功响应:
{
"ctrl": {
"id": "login-1",
"code": 200,
"text": "ok",
"params": {
"user": "usrUser001",
"token": "token-string",
"expires": "2026-08-30T08:00:00.000Z"
},
"ts": "2026-07-31T08:00:00.000Z"
}
}
Token 自动登录
{"login":{"id":"login-token-1","scheme":"token","secret":"token-string"}}
注册
{
"acc": {
"id": "acc-1",
"user": "new",
"scheme": "basic",
"secret": "MTg4MjY4Mjc3NTQ6MTIzNDU2",
"login": true,
"tags": ["tel:+8618826827754", "basic:18826827754"],
"cred": [
{"meth": "tel", "val": "+8618826827754", "resp": "123456"}
],
"desc": {
"defacs": {"auth": "JRWS", "anon": "N"},
"public": {
"fn": "用户1882",
"x-im-area": "广东省广州市"
},
"private": {
"x-im-settings": {
"pushEnabled": true,
"teenMode": false
}
}
}
}
}
忘记密码
发送重置请求:
{"login":{"id":"reset-1","scheme":"reset","secret":"YmFzaWM6dGVsOis4NjE4ODI2ODI3NzU0"}}
使用收到的临时 token 修改密码:
{
"acc": {
"id": "reset-2",
"token": "restricted-reset-token",
"scheme": "basic",
"secret": "OjEyMzQ1Njc4"
}
}
实名认证
实名认证是业务扩展,建议写入用户 private.x-im-realname,服务端审核通过后可由 root 写入 trusted.verified=true。
{
"set": {
"id": "realname-1",
"topic": "me",
"desc": {
"private": {
"x-im-realname": {
"name": "张三",
"idCard": "440100199001011234",
"status": "pending"
}
}
}
}
}
服务端返回:
{"ctrl":{"id":"realname-1","topic":"me","code":200,"text":"ok","ts":"2026-07-31T08:00:00.000Z"}}
消息与会话
获取消息列表
原型页面:消息、文件助手、系统消息、聊天会话列表。
订阅 me 后获取当前用户订阅的所有会话:
{"get":{"id":"contacts-1","topic":"me","what":"sub","sub":{"limit":50}}}
响应:
{
"meta": {
"id": "contacts-1",
"topic": "me",
"sub": [
{
"topic": "usrPeer001",
"updated": "2026-07-31T08:00:00.000Z",
"touched": "2026-07-31T08:10:00.000Z",
"seq": 18,
"read": 15,
"recv": 18,
"online": true,
"public": {"fn": "王子民", "photo": {"ref": "/v0/file/s/a.jpg"}},
"private": {"comment": "王子民", "x-im-muted": false, "x-im-pinned": true}
}
]
}
}
进入私聊
私聊 topic 使用对方用户 ID。首次 {sub} 会创建 P2P topic。
{
"sub": {
"id": "p2p-sub-1",
"topic": "usrPeer001",
"get": {
"what": "desc sub data",
"data": {"limit": 20}
}
}
}
进入群聊
{
"sub": {
"id": "grp-sub-1",
"topic": "grpTeam001",
"get": {
"what": "desc sub data aux",
"data": {"limit": 20}
}
}
}
发送文本消息
Tinode 推荐使用 Drafty。发送 Drafty 时必须设置 head.mime="text/x-drafty"。
{
"pub": {
"id": "pub-text-1",
"topic": "usrPeer001",
"head": {
"mime": "text/x-drafty",
"x-im-type": "text",
"x-im-client-mid": "local-001"
},
"content": {
"txt": "你好,我是王理娟,很高兴认识你,你怎么称呼"
}
}
}
接收方收到:
{
"data": {
"topic": "usrPeer001",
"from": "usrUser001",
"seq": 19,
"ts": "2026-07-31T08:10:00.000Z",
"head": {
"mime": "text/x-drafty",
"x-im-type": "text",
"x-im-client-mid": "local-001"
},
"content": {
"txt": "你好,我是王理娟,很高兴认识你,你怎么称呼"
}
}
}
已收、已读、未读数
原型页面:个人对话已读状态、消息未读红点。
{"note":{"topic":"usrPeer001","what":"recv","seq":19,"unread":3}}
{"note":{"topic":"usrPeer001","what":"read","seq":19,"unread":0}}
对端收到:
{"info":{"topic":"usrPeer001","from":"usrPeer002","what":"read","seq":19}}
输入中与录音中
{"note":{"topic":"usrPeer001","what":"kp"}}
{"note":{"topic":"usrPeer001","what":"kpa"}}
{"note":{"topic":"usrPeer001","what":"kpv"}}
@ 提醒
原型页面:@人员-选择提醒人、@人员—选择提醒人-多选。
{
"pub": {
"id": "pub-mention-1",
"topic": "grpTeam001",
"head": {
"mime": "text/x-drafty",
"mentions": ["usrA001", "usrB001"],
"x-im-type": "text"
},
"content": {
"txt": "@万先生 @钱哥你们看一下",
"fmt": [
{"at": 0, "len": 4, "key": 0},
{"at": 5, "len": 3, "key": 1}
],
"ent": [
{"tp": "MN", "data": {"val": "usrA001"}},
{"tp": "MN", "data": {"val": "usrB001"}}
]
}
}
}
回复、引用、编辑
长按菜单中的引用、回复、编辑沿用 Tinode head.reply、head.replace。
{
"pub": {
"id": "pub-reply-1",
"topic": "grpTeam001",
"head": {
"mime": "text/x-drafty",
"reply": "grpTeam001:19",
"x-im-type": "text"
},
"content": {"txt": "我同意这个方案"}
}
}
编辑消息:
{
"pub": {
"id": "pub-edit-1",
"topic": "grpTeam001",
"head": {
"mime": "text/x-drafty",
"replace": ":19",
"x-im-type": "text"
},
"content": {"txt": "修改后的消息内容"}
}
}
撤回与删除
单聊/群聊撤回使用硬删除,前提是用户有 D 权限或符合后端业务规则。
{
"del": {
"id": "del-msg-1",
"topic": "grpTeam001",
"what": "msg",
"hard": true,
"delseq": [{"low": 19}]
}
}
仅自己删除:
{
"del": {
"id": "del-msg-self-1",
"topic": "grpTeam001",
"what": "msg",
"hard": false,
"delseq": [{"low": 19}]
}
}
转发
逐条转发:
{
"pub": {
"id": "forward-1",
"topic": "usrPeer002",
"head": {
"mime": "text/x-drafty",
"forwarded": "grpTeam001:19",
"x-im-type": "text"
},
"content": {"txt": "被转发的内容"}
}
}
合并转发:
{
"pub": {
"id": "forward-merge-1",
"topic": "usrPeer002",
"head": {
"mime": "application/json",
"x-im-type": "forward-bundle"
},
"content": {
"title": "群聊的聊天记录",
"items": [
{"topic": "grpTeam001", "seq": 19, "from": "usrA001", "text": "第一条"},
{"topic": "grpTeam001", "seq": 20, "from": "usrB001", "text": "第二条"}
]
}
}
}
好友与通讯录
查找用户
原型页面:添加好友、输入精准用户 ID。
使用 fnd topic 查询标签。手机号、用户名、用户 ID 由后端查询重写或精确匹配。
{
"set": {
"id": "find-1",
"topic": "fnd",
"desc": {
"public": "18826827754"
}
}
}
{"get":{"id":"find-2","topic":"fnd","what":"sub","sub":{"limit":20}}}
添加好友申请
P2P 建立关系使用 {sub topic=userId}。申请备注放入 set.desc.private.x-im-applyText,审批状态放入 private.x-im-applyStatus。服务端会校验状态枚举,真实可聊权限仍以 Tinode ACL 为准。
{
"sub": {
"id": "friend-apply-1",
"topic": "usrPeer001",
"set": {
"desc": {
"private": {
"x-im-applyText": "我是王子民",
"x-im-applyStatus": "pending"
}
}
},
"get": {"what": "desc"}
}
}
对方在线时会收到 me topic 的 pres what="acs";客户端可据此刷新新朋友列表。
新朋友列表
客户端从 me 的订阅列表中过滤待处理 P2P 权限:
{"get":{"id":"new-friends-1","topic":"me","what":"sub","sub":{"limit":100}}}
列表状态建议:
| 条件 | 含义 |
|---|---|
private.x-im-applyStatus="pending" |
申请待处理 |
private.x-im-applyStatus="accepted" |
申请已同意 |
private.x-im-applyStatus="rejected" |
申请已拒绝 |
对方订阅变更产生 pres what="acs" |
我收到的新申请 |
同意好友
{
"set": {
"id": "friend-approve-1",
"topic": "usrPeer001",
"sub": {
"user": "usrPeer001",
"mode": "JRWPA"
}
}
}
同意后建议双方把各自订阅 private.x-im-applyStatus 更新为 accepted;拒绝时更新为 rejected。非法状态会返回 400 malformed。
设置备注
{
"set": {
"id": "remark-1",
"topic": "usrPeer001",
"desc": {
"private": {
"comment": "王子民",
"x-im-remarkUpdatedAt": "2026-07-31T08:00:00.000Z"
}
}
}
}
拉黑与取消拉黑
客户端私有状态用于 UI 展示,服务端应同时调整 P2P ACL,阻止对方继续发起会话。
{
"set": {
"id": "block-1",
"topic": "usrPeer001",
"desc": {
"private": {
"x-im-blocked": true
}
},
"sub": {
"user": "usrPeer001",
"mode": "JRPA"
}
}
}
取消拉黑:
{
"set": {
"id": "unblock-1",
"topic": "usrPeer001",
"desc": {
"private": {
"x-im-blocked": false
}
},
"sub": {
"user": "usrPeer001",
"mode": "JRWPA"
}
}
}
删除好友
{
"leave": {
"id": "friend-delete-1",
"topic": "usrPeer001",
"unsub": true
}
}
群聊与群管理
创建群聊
原型页面:创建群聊。
{
"sub": {
"id": "group-create-1",
"topic": "new",
"set": {
"desc": {
"defacs": {"auth": "JRWP", "anon": "N"},
"public": {
"fn": "新人交流群",
"note": "群主很懒,还没有群简介"
},
"private": {}
},
"aux": {
"x-im-group": {
"joinApproval": false,
"onlyAdminInvite": false,
"allowMemberFileUpload": true,
"muteAll": false,
"mutedUsers": []
}
}
},
"get": {"what": "desc sub aux"}
}
}
成功响应的 ctrl.topic 为新群 ID:
说明:创建群时传入的 set.aux.x-im-group 会写入 topics.aux,后续可通过 {get what="aux"} 或 {sub get.what="desc sub aux"} 读取。
{"ctrl":{"id":"group-create-1","topic":"grpNew001","code":200,"text":"ok","ts":"2026-07-31T08:00:00.000Z"}}
邀请成员入群
原型页面:邀请进群申请、开启群验证拉人状态。
无需审批时直接共享订阅:
如果 aux.x-im-group.onlyAdminInvite=true,服务端只允许群管理员或群主执行邀请。
{
"set": {
"id": "group-invite-1",
"topic": "grpNew001",
"sub": {
"user": "usrInvitee001",
"mode": "JRWP"
}
}
}
需要审批时,申请人发送 {sub},请求 JRWPS,申请备注写入自己的订阅 private.x-im-joinApplyText。若群 aux.x-im-group.joinApproval=true 且默认 defacs.auth 不包含 J,服务端会保存一条 want=JRWPS/given=N 的待审批订阅并返回 200 ok,但不会把申请人 attach 到群。
{
"sub": {
"id": "group-join-apply-1",
"topic": "grpNew001",
"set": {
"sub": {"mode": "JRWPS"},
"desc": {
"private": {
"x-im-joinApplyText": "申请加入群聊",
"x-im-joinStatus": "pending"
}
}
}
}
}
管理员获取待审批列表:
{"get":{"id":"group-apply-list-1","topic":"grpNew001","what":"sub","sub":{"limit":100}}}
返回中待审批用户表现为 acs.want 包含 J、acs.given="N"、private.x-im-joinStatus="pending":
{
"meta": {
"id": "group-apply-list-1",
"topic": "grpNew001",
"sub": [
{
"user": "usrInvitee001",
"acs": {"want": "JRWPS", "given": "N", "mode": "N"},
"private": {
"x-im-joinApplyText": "申请加入群聊",
"x-im-joinStatus": "pending"
}
}
]
}
}
管理员审批通过:
{
"set": {
"id": "group-approve-1",
"topic": "grpNew001",
"sub": {
"user": "usrInvitee001",
"mode": "JRWP"
}
}
}
服务端会把申请人的 private.x-im-joinStatus 自动更新为 accepted。如果拒绝,管理员设置 mode="N",服务端会把状态更新为 rejected:
{
"set": {
"id": "group-reject-1",
"topic": "grpNew001",
"sub": {
"user": "usrInvitee001",
"mode": "N"
}
}
}
如需在群内留痕,管理员可额外发布审批结果消息:
{
"pub": {
"id": "group-join-decision-1",
"topic": "grpNew001",
"head": {
"mime": "application/json",
"x-im-type": "group-join-approve"
},
"content": {
"applicant": "usrInvitee001",
"status": "accepted",
"reviewedAt": "2026-07-31T08:00:00.000Z"
}
}
}
群资料与群公告
更新群名称、头像、简介:
{
"set": {
"id": "group-profile-1",
"topic": "grpNew001",
"desc": {
"public": {
"fn": "新人交流群",
"photo": {"ref": "/v0/file/s/group-avatar.jpg", "type": "image/jpeg"},
"note": "欢迎交流,禁止广告。"
}
}
}
}
更新群公告:
{
"set": {
"id": "group-ann-1",
"topic": "grpNew001",
"aux": {
"x-im-group": {
"announcement": {
"text": "为帮助新用户熟悉群规则,请勿发布违规内容。",
"updatedBy": "usrOwner001",
"updatedAt": "2026-07-31T08:00:00.000Z"
}
}
}
}
}
公告也可以发送一条系统样式消息,方便聊天页展示:
{
"pub": {
"id": "group-ann-msg-1",
"topic": "grpNew001",
"head": {
"mime": "application/json",
"x-im-type": "group-announcement"
},
"content": {
"text": "为帮助新用户熟悉群规则,请勿发布违规内容。"
}
}
}
设置管理员
原型页面:设置群管理员、添加管理员、删除管理员。
当前后端 {set.sub.mode} 使用绝对权限字符串,不支持 +A、-A 这类增量写法。前端需要按目标最终权限发送完整 mode;用户有效权限为 want & given,因此授予管理员时通常需要管理员设置目标用户的 given,目标用户再通过自己的 {set.sub} 提升 want。
授予管理员权限:
{
"set": {
"id": "admin-add-1",
"topic": "grpNew001",
"sub": {
"user": "usrAdmin001",
"mode": "JRWPASD"
}
}
}
移除管理员权限:
{
"set": {
"id": "admin-del-1",
"topic": "grpNew001",
"sub": {
"user": "usrAdmin001",
"mode": "JRWPS"
}
}
}
群禁言
禁言单个成员建议同时更新 ACL 与 aux.x-im-group.mutedUsers。第一阶段服务端会在发送消息时检查 mutedUsers,普通成员在列表中会被拒绝;管理员和群主不受该业务禁言影响。
{
"set": {
"id": "mute-user-1",
"topic": "grpNew001",
"sub": {
"user": "usrMuted001",
"mode": "JRP"
},
"aux": {
"x-im-group": {
"mutedUsers": ["usrMuted001"]
}
}
}
}
解除禁言:
{
"set": {
"id": "unmute-user-1",
"topic": "grpNew001",
"sub": {
"user": "usrMuted001",
"mode": "JRWP"
},
"aux": {
"x-im-group": {
"mutedUsers": []
}
}
}
}
全员禁言:
第一阶段服务端会拒绝普通成员发送消息,管理员和群主仍可发送。
{
"set": {
"id": "mute-all-1",
"topic": "grpNew001",
"aux": {
"x-im-group": {
"muteAll": true
}
}
}
}
群文件上传权限
当 aux.x-im-group.allowMemberFileUpload=false 时,服务端会拒绝普通成员发送 x-im-type 为 image、audio、video、file 的消息,或带 extra.attachments 的消息。管理员和群主不受限制。
{
"set": {
"id": "group-file-policy-1",
"topic": "grpNew001",
"aux": {
"x-im-group": {
"allowMemberFileUpload": false
}
}
}
}
群主转让
原型页面:选择新群主、选择新群主提示。
第一步,当前群主发起转让,把新群主的 given 权限设置为包含 O 的完整权限:
{
"set": {
"id": "owner-transfer-1",
"topic": "grpNew001",
"sub": {
"user": "usrNewOwner001",
"mode": "JRWPASDO"
}
}
}
第二步,新群主接受转让,把自己的 want 权限设置为包含 O 的完整权限:
{
"set": {
"id": "owner-accept-1",
"topic": "grpNew001",
"sub": {
"mode": "JRWPASDO"
}
}
}
后端必须保证同一 topic 只有一个 O 权限用户。当前 Tinode 实际行为是:新群主接受后,服务端自动移除旧群主的 O,并向双方推送 pres what="acs"。
踢出群成员
{
"del": {
"id": "kick-1",
"topic": "grpNew001",
"what": "sub",
"user": "usrTarget001",
"hard": false
}
}
退出群聊
{
"leave": {
"id": "leave-group-1",
"topic": "grpNew001",
"unsub": true
}
}
文件、收藏与聊天记录
上传并发送图片
原型页面:单张图片状态、图片和语音发送中。
- 客户端通过
/v0/file/u/上传图片,获得ctrl.params.url。 - 通过 WebSocket
{pub}发送 Drafty 图片消息。
{
"pub": {
"id": "pub-img-1",
"topic": "usrPeer001",
"head": {
"mime": "text/x-drafty",
"attachments": ["/v0/file/s/img001.jpg"],
"x-im-type": "image",
"x-im-client-mid": "local-img-1"
},
"content": {
"txt": " ",
"fmt": [{"at": -1, "len": 1, "key": 0}],
"ent": [
{
"tp": "IM",
"data": {
"mime": "image/jpeg",
"ref": "/v0/file/s/img001.jpg",
"width": 1080,
"height": 1920,
"size": 245760
}
}
]
}
},
"extra": {
"attachments": ["/v0/file/s/img001.jpg"]
}
}
发送普通文件
原型页面:文件发送状态、文件下载、文件打开。
{
"pub": {
"id": "pub-file-1",
"topic": "usrPeer001",
"head": {
"mime": "text/x-drafty",
"attachments": ["/v0/file/s/doc001.pdf"],
"x-im-type": "file"
},
"content": {
"txt": "365天天高清晰记忆曲线复习_pdf",
"fmt": [{"at": -1, "len": 0, "key": 0}],
"ent": [
{
"tp": "EX",
"data": {
"mime": "application/pdf",
"ref": "/v0/file/s/doc001.pdf",
"name": "365天天高清晰记忆曲线复习_pdf",
"size": 115000
}
}
]
}
},
"extra": {
"attachments": ["/v0/file/s/doc001.pdf"]
}
}
发送语音消息
{
"pub": {
"id": "pub-audio-1",
"topic": "usrPeer001",
"head": {
"mime": "text/x-drafty",
"attachments": ["/v0/file/s/audio001.m4a"],
"x-im-type": "audio"
},
"content": {
"txt": " ",
"fmt": [{"at": -1, "len": 1, "key": 0}],
"ent": [
{
"tp": "AU",
"data": {
"mime": "audio/mp4",
"ref": "/v0/file/s/audio001.m4a",
"duration": 6000,
"name": "voice.m4a",
"size": 48000
}
}
]
}
},
"extra": {
"attachments": ["/v0/file/s/audio001.m4a"]
}
}
群文件列表
群文件是群消息附件的业务视图。二期 MySQL 后端已建立 im_message_index,带 x-im-filter 或 x-im-search 的 {get data} 会优先走索引查询;其它未实现索引的数据库后端仍回退到一阶段内存过滤。
扩展查询:
{
"get": {
"id": "group-files-1",
"topic": "grpNew001",
"what": "data",
"data": {
"limit": 50
},
"x-im-filter": {
"types": ["file"],
"keyword": "复习"
}
}
}
收藏消息
原型页面:我的收藏、收藏文本、收藏文件、收藏语音、收藏链接。
建议收藏使用 slf topic 存储,head.x-im-type="favorite",原始消息定位放在内容里。
{
"pub": {
"id": "fav-1",
"topic": "slf",
"head": {
"mime": "application/json",
"x-im-type": "favorite"
},
"content": {
"sourceTopic": "grpNew001",
"sourceSeq": 19,
"kind": "file",
"title": "365天天高清晰记忆曲线复习_pdf",
"summary": "PDF 115KB",
"collectedAt": "2026-07-31T08:00:00.000Z"
}
}
}
获取收藏:
{
"sub": {
"id": "fav-list-1",
"topic": "slf",
"get": {
"what": "data",
"data": {"limit": 50}
}
}
}
查找聊天记录
原型页面:查看聊天记录-全部、图片、视频、文件、链接、搜索关键词。
Tinode 原生 {get what="data"} 支持按 seq 分页,不直接支持关键词搜索。关键词、类型搜索需要后端扩展索引。
二期以后 MySQL 后端会优先查询 im_message_index;当前 schema 119 对 search_text 增加 FULLTEXT 索引,同时保留 LIKE fallback,避免中文短词或未命中全文分词时漏查。
{
"get": {
"id": "search-msg-1",
"topic": "usrPeer001",
"what": "data",
"data": {"limit": 50},
"x-im-search": {
"keyword": "装修",
"types": ["text", "image", "file", "link"],
"before": 0
}
}
}
语音、视频与通话
发起语音通话
原型页面:语音通话-对方待接受状态、双方语音沟通中。
Tinode 原生通话流程为:先用 {pub} 发送 head.webrtc="started" 的通话邀请并持久化一条通话消息;后续信令用 {note what="call"}。本产品在通话消息内容与 note.payload 中约定 callId、callType。
后端校验规则:显式发送 head.x-im-type="call" 且 head.webrtc="started" 的产品通话邀请,content.callId 必填,否则返回 400 malformed。ringing、accept、hang-up 控制事件的 payload.callId 必填;offer、answer、ice-candidate 仍按 Tinode 原生 WebRTC 信令透传,避免破坏 SDP/ICE 结构。
{
"pub": {
"id": "call-audio-1",
"topic": "usrPeer001",
"head": {
"mime": "application/json",
"webrtc": "started",
"x-im-type": "call"
},
"content": {
"callId": "call-001",
"callType": "audio",
"audio": true,
"video": false
}
}
}
发起视频通话
{
"pub": {
"id": "call-video-1",
"topic": "usrPeer001",
"head": {
"mime": "application/json",
"webrtc": "started",
"x-im-type": "call"
},
"content": {
"callId": "call-002",
"callType": "video",
"audio": true,
"video": true,
"cameraOn": true
}
}
}
接受、拒绝、挂断
接受:
{
"note": {
"topic": "usrPeer001",
"what": "call",
"event": "accept",
"seq": 102,
"payload": {
"callId": "call-002"
}
}
}
拒绝或挂断使用 Tinode 原生 hang-up 事件;通话尚未接通时服务端会把最终消息标记为 declined/missed。
{
"note": {
"topic": "usrPeer001",
"what": "call",
"event": "hang-up",
"seq": 102,
"payload": {
"callId": "call-002",
"reason": "user_declined"
}
}
}
挂断:
{
"note": {
"topic": "usrPeer001",
"what": "call",
"event": "hang-up",
"seq": 102,
"payload": {
"callId": "call-002",
"duration": 65000
}
}
}
通话状态消息
通话结束后发送一条持久消息用于聊天记录展示。
{
"pub": {
"id": "call-record-1",
"topic": "usrPeer001",
"head": {
"mime": "application/json",
"webrtc": "finished",
"webrtc-duration": 65000,
"x-im-type": "call"
},
"content": {
"callId": "call-002",
"callType": "video",
"status": "finished",
"duration": 65000
}
}
}
SDP 与 ICE 信令
通话被接受后,WebRTC 元数据使用 Tinode 原生 offer、answer、ice-candidate 事件转发。麦克风、扬声器、摄像头开关属于本地 UI 状态;如后续需要跨端同步,应在 server/calls.go 明确扩展事件白名单。
{
"note": {
"topic": "usrPeer001",
"what": "call",
"event": "ice-candidate",
"seq": 102,
"payload": {
"callId": "call-002",
"candidate": "candidate:..."
}
}
}
话题功能
话题不是 Tinode 原生 topic 的“群聊 topic”,而是产品信息流/活动报名类业务。建议实现为频道 chn,每个话题详情页对应一个频道 topic;话题列表由 fnd 搜索或专用系统频道承载。
发布话题
原型页面:发布话题、话题类型选择、话题人数、积分不足。
二期 MySQL 后端已接入话题发布积分扣减:创建 nch 频道时,如果 public.x-im-topic.pointsCost 大于 0,服务端会在同一事务中创建话题、创建作者订阅并扣减 im_user_points.balance。余额不足返回 422 policy violation,话题不会创建;普通群聊 {sub topic="new"} 不受该规则影响。
{
"sub": {
"id": "topic-create-1",
"topic": "nch",
"set": {
"desc": {
"defacs": {"auth": "R", "anon": "N"},
"public": {
"fn": "国防办举行高质量完成十四五规划系列主题新闻发布会",
"photo": {"ref": "/v0/file/s/topic-cover.jpg", "type": "image/jpeg"},
"note": "话题摘要内容",
"x-im-topic": {
"type": "news",
"labels": ["美食", "天气"],
"durationHours": 3,
"participantLimit": 20,
"pointsCost": 234,
"joinEnabled": true
}
}
},
"tags": ["topic", "topic:news", "美食", "天气"],
"aux": {
"x-im-topic": {
"participants": [],
"status": "open"
}
}
},
"get": {"what": "desc aux"}
}
}
获取话题列表
{
"set": {
"id": "topic-search-1",
"topic": "fnd",
"desc": {
"public": "topic news"
}
}
}
{"get":{"id":"topic-search-2","topic":"fnd","what":"sub","sub":{"limit":20}}}
推荐话题列表
推荐排序仍复用 Tinode fnd topic,通过 {get what="sub"} 附带 x-im-recommend 传入推荐条件。MySQL 后端当前按关键词、话题类型、标签与订阅数做轻量排序;频道型话题会按 Tinode 规范以 chnXXX 返回。
{
"get": {
"id": "topic-recommend-1",
"topic": "fnd",
"what": "sub",
"sub": {"limit": 20},
"x-im-recommend": {
"keyword": "发布会",
"types": ["news"],
"labels": ["美食"],
"limit": 10
}
}
}
响应示例:
{
"meta": {
"id": "topic-recommend-1",
"topic": "fnd",
"sub": [
{
"topic": "chnTopic001",
"public": {
"fn": "本周美食发布会",
"x-im-topic": {
"type": "news",
"labels": ["美食", "活动"]
}
},
"private": ["topic", "topic:news", "美食"],
"subcnt": 128
}
]
}
}
参与话题
话题详情页点击“参与话题”时订阅频道。
{
"sub": {
"id": "topic-join-1",
"topic": "chnTopic001",
"set": {
"sub": {"mode": "R"}
},
"get": {"what": "desc data aux"}
}
}
若要发送评论,需要后端根据业务授予 W 权限。
转发帖子
{
"pub": {
"id": "topic-forward-1",
"topic": "usrPeer001",
"head": {
"mime": "application/json",
"x-im-type": "topic-share"
},
"content": {
"topic": "chnTopic001",
"title": "国防办举行高质量完成十四五规划系列主题新闻发布会",
"cover": "/v0/file/s/topic-cover.jpg",
"summary": "话题摘要内容"
}
}
}
系统消息、举报与安全提示
系统消息
系统消息建议使用 sys topic 或专用系统账号向用户 P2P 发送。客户端消息列表可将系统账号展示为“系统消息”。
{
"data": {
"topic": "usrSystem001",
"from": "usrSystem001",
"seq": 1,
"head": {
"mime": "application/json",
"x-im-type": "system-notice"
},
"content": {
"title": "平台整治净化治理公告",
"summary": "国防办举行高质量完成十四五规划系列...",
"url": "/notice/20260731"
},
"ts": "2026-07-31T08:00:00.000Z"
}
}
举报
原型页面:举报、举报原因列表。
普通用户可向 sys 发布举报内容。
{
"pub": {
"id": "report-1",
"topic": "sys",
"head": {
"mime": "application/json",
"x-im-type": "report"
},
"content": {
"targetType": "message",
"targetUser": "usrBad001",
"targetTopic": "grpNew001",
"targetSeq": 19,
"reason": "发布违法违规内容",
"description": "群成员发布诈骗信息",
"evidence": ["/v0/file/s/evidence001.jpg"]
}
},
"extra": {
"attachments": ["/v0/file/s/evidence001.jpg"]
}
}
反诈与安全提醒
反诈弹窗可由服务端在消息 head 上标记,前端收到后根据策略展示一次或二次确认。当前后端已实现轻量规则 MVP:疑似转账、验证码、外链、违禁词等命中后不阻断消息,只追加风险和审核标记,便于前端提示与后台后续处理。
{
"data": {
"topic": "usrPeer001",
"from": "usrPeer001",
"seq": 20,
"head": {
"mime": "text/x-drafty",
"x-im-type": "text",
"x-im-anti-fraud": true,
"x-im-risk-level": "high",
"x-im-moderation": {
"status": "flagged",
"reasons": ["payment-scam", "credential-risk", "external-link"],
"reviewedAt": "2026-07-31T08:00:00.000Z"
}
},
"content": {"txt": "请点击链接完成转账"},
"ts": "2026-07-31T08:00:00.000Z"
}
}
客户端确认提醒后可发送临时回执:
{
"note": {
"topic": "usrPeer001",
"what": "data",
"payload": {
"x-im-event": "anti-fraud-confirmed",
"seq": 20
}
}
}
个人中心与设置
获取个人资料
{"get":{"id":"profile-1","topic":"me","what":"desc tags cred"}}
修改昵称、头像、生日、地区、个性签名
{
"set": {
"id": "profile-update-1",
"topic": "me",
"desc": {
"public": {
"fn": "林南",
"photo": {"ref": "/v0/file/s/avatar.jpg", "type": "image/jpeg"},
"note": "成人最懂什么都没留下",
"x-im-gender": "female",
"x-im-birthday": "2008-02-07",
"x-im-area": "广东省广州市"
}
}
},
"extra": {
"attachments": ["/v0/file/s/avatar.jpg"]
}
}
置顶、免打扰、清空聊天记录
置顶写入 me.private.tpins 或当前 topic private.x-im-pinned。
{
"set": {
"id": "pin-1",
"topic": "usrPeer001",
"desc": {
"private": {
"x-im-pinned": true
}
}
}
}
免打扰:
{
"set": {
"id": "mute-topic-1",
"topic": "usrPeer001",
"desc": {
"private": {
"x-im-muted": true
}
}
}
}
清空聊天记录:
{
"del": {
"id": "clear-local-1",
"topic": "usrPeer001",
"what": "msg",
"hard": false,
"delseq": [{"low": 1, "hi": 2147483647}]
}
}
隐私设置
原型页面:隐私设置-添加我的方式选项。
{
"set": {
"id": "privacy-1",
"topic": "me",
"desc": {
"private": {
"x-im-settings": {
"allowSearchByCard": true,
"allowSearchByQr": true,
"allowSearchByGroup": true,
"allowSearchByPhone": false,
"joinGroupNeedsApproval": true
}
}
}
}
}
青少年模式
{
"set": {
"id": "teen-1",
"topic": "me",
"desc": {
"private": {
"x-im-settings": {
"teenMode": true,
"teenModeEnabledAt": "2026-07-31T08:00:00.000Z"
}
}
}
}
}
签到任务
签到属于业务扩展,使用 slf topic 记录用户行为,同时后端更新积分系统。
二期 MySQL 后端已接入 head.x-im-type="checkin" 的 {pub} 处理:同一用户、同一自然日、同一 event_type 只记录一次,首次签到成功后固定增加 5 积分并保存一条 slf 消息;重复签到返回 304 no action,不重复加分,也不保存重复消息。前后端通信仍保持 WebSocket {pub} 形态。
{
"pub": {
"id": "checkin-1",
"topic": "slf",
"head": {
"mime": "application/json",
"x-im-type": "checkin"
},
"content": {
"date": "2026-07-31",
"points": 5
}
}
}
重复签到响应示例:
{
"ctrl": {
"id": "checkin-1",
"topic": "slf",
"code": 304,
"text": "no action",
"params": {
"x-im-checkin": {
"already": true,
"date": "2026-07-31",
"points": 5,
"balance": 35
}
}
}
}
意见反馈
{
"pub": {
"id": "feedback-1",
"topic": "sys",
"head": {
"mime": "application/json",
"x-im-type": "feedback"
},
"content": {
"type": "功能建议",
"text": "希望优化消息搜索",
"images": ["/v0/file/s/feedback001.jpg"]
}
},
"extra": {
"attachments": ["/v0/file/s/feedback001.jpg"]
}
}
错误码与状态码
Tinode {ctrl.code} 遵循 HTTP 状态码模型。
| code | 含义 | 前端处理 |
|---|---|---|
200 |
成功 | 更新本地状态 |
201 |
已创建 | 使用返回的 topic 或 params |
300 |
需要额外验证 | 展示验证码/身份验证页面 |
400 |
请求格式错误 | 检查 JSON 和字段 |
401 |
未登录或 Token 失效 | 重新登录 |
403 |
无权限 | 展示“无权限”或禁言/拉黑提示 |
404 |
资源不存在 | 提示用户不存在、群不存在 |
409 |
状态冲突 | 已申请、已加入、重复操作 |
413 |
消息或文件过大 | 提示压缩或重新选择 |
423 |
Topic 锁定/处理中 | 稍后重试 |
500 |
服务端错误 | 展示通用错误并上报 |
503 |
队列满或服务不可用 | 自动重试或稍后重试 |
业务状态枚举
| 字段 | 值 | 含义 |
|---|---|---|
x-im-type |
text |
文本 |
x-im-type |
image |
图片 |
x-im-type |
audio |
语音 |
x-im-type |
video |
视频 |
x-im-type |
file |
文件 |
x-im-type |
card |
名片 |
x-im-type |
favorite |
收藏 |
x-im-type |
forward-bundle |
合并转发 |
x-im-type |
group-join-apply |
加群申请 |
x-im-type |
group-join-approve |
加群审批通过 |
x-im-type |
group-join-reject |
加群审批拒绝 |
x-im-type |
group-announcement |
群公告 |
x-im-type |
call |
通话记录 |
x-im-type |
topic-share |
话题分享 |
x-im-type |
system-notice |
系统通知 |
x-im-type |
report |
举报 |
x-im-type |
feedback |
意见反馈 |
x-im-type |
checkin |
签到 |
前端发送状态建议
| UI 状态 | 判定方式 |
|---|---|
| 发送中 | 本地消息已入队,尚未收到 {ctrl} 或 {data} echo |
| 发送成功 | 收到同 id 的 {ctrl.code=200} 或同 x-im-client-mid 的 {data} |
| 发送失败 | 收到 {ctrl.code>=400} 或超时 |
| 已送达 | 收到对端 {info.what="recv"} |
| 已读 | 收到对端 {info.what="read"} |
| 已撤回 | 收到 {pres.what="del"} 或 {meta.del} 包含对应 seq |
后端实现提示
- 原生 Tinode 已支持的字段优先复用:
public、private、aux、head.mentions、head.reply、head.replace、head.forwarded、extra.attachments。 - 业务扩展统一从
x-im-*读取和写入,避免污染 Tinode 原生字段。 - 需要服务端索引的功能包括:聊天记录关键词搜索、按附件类型搜索、收藏分类、话题列表、积分扣减、反诈风险标记。
- 所有 WebSocket 消息必须是严格 JSON,示例中的字段名和字符串在真实请求中都需要双引号。
- 文件上传仍使用 Tinode 大文件接口;上传后的文件引用必须写入
extra.attachments,否则服务端 GC 可能清理未引用文件。 - Phase 3 当前为后端内置 MVP:反诈和内容审核只做规则标记,推荐只做 MySQL 轻量排序,复杂模型、人工审核工作台、异步搜索服务仍可后续独立演进。