在Kafka消息持久化重试耗尽时,新增将消息死信的逻辑,而非保留未提交的偏移量,避免无法解决的故障导致分区消息无限重试阻塞后续消息。同时补充死信处理的日志,确保问题可追踪。
266 KiB
IM 前后端逐图对接协议
版本:v2.0
日期:2026-08-11
需求依据:IM 产品原型需求文档
设计依据:IM 系统设计文档
说明:第 1 至第 11 章的章节编号、图片标题和顺序与产品原型完全一致。每张图均给出可直接开发、联调和验收的协议。
0. 公共对接约定
0.1 实现状态
| 状态 | 含义 |
|---|---|
已实现 |
当前服务端已有能力,可按本文联调 |
需改造 |
已有基础能力,但字段、鉴权或业务规则需要调整 |
新增 |
当前服务端没有该产品接口或数据模型 |
纯客户端 |
界面或系统能力,不调用服务端;关联的提交协议会单独指出 |
文档中所有 新增、需改造 接口都是目标契约,不代表当前代码已经可用。前后端必须以本文字段为准实施,接口完成前可使用同结构 Mock。
0.2 API Key、启动入口与通道
0.2.1 现有系统的 API Key 从哪里来
API Key 不是客户端启动后向 IM 服务端申请得到的值。现有代码没有“获取 API Key”的 HTTP 或 WebSocket 接口。
API Key 是应用级接入标识,由部署人员在发布客户端前生成并配置:
- 新部署同时生成 API Key 和 HMAC Salt:在项目根目录执行
go run ./keygen。 - 已有服务端继续使用当前
api_key_salt时,执行go run ./keygen -sequence <新序号> -salt '<服务端 api_key_salt>'生成兼容的普通应用 Key。 - 输出中的
HMAC salt配置到服务端server/meetingim.conf的api_key_salt;输出中的API key配置到 iOS、Android 或 Web 客户端构建配置/受控环境配置。 - 客户端启动时从自己的应用配置读取 API Key,不调用 IM 接口获取。禁止把
-isroot 1生成的 Root Key 放进客户端。
生成示意:
$ go run ./keygen
API key v1 seq1 [ordinary]: <client-api-key>
HMAC salt: <server-hmac-salt>
API Key、企业码和用户 Token 是三个不同概念:
| 值 | 取得时间 | 作用 | 是否代表用户身份 |
|---|---|---|---|
api_key |
应用发布/环境配置时预置 | 允许应用访问 MeetingIM 入口 | 否 |
tenant_code |
用户输入企业码 | 将 Session 路由并绑定到企业 | 否 |
token |
用户登录成功后由服务端返回 | 标识当前租户内的登录用户 | 是 |
API Key 可以被客户端应用提取,因此不能把它当成用户密码或租户授权。现有 checkAPIKey 只校验 Key 的格式和 HMAC 签名;最终权限仍必须依赖租户 Session、用户 Token 和 Topic/业务 ACL。
当前 checkAPIKey 没有按 sequence 吊销旧 Key 的逻辑。生成新序号不会自动让旧 Key 失效;若需要精细轮换,服务端必须增加最低可接受序号或撤销表。直接更换 api_key_salt 会让所有旧 Key 同时失效,必须与客户端升级协调。
0.2.2 客户端怎样携带 API Key
| 通道 | 地址 | 认证 | 用途 |
|---|---|---|---|
| WebSocket(浏览器/通用方式) | wss://{host}/{api_path}v0/channels?apikey={api_key} |
Upgrade 校验 API Key,hi 绑定租户,login 登录 |
Topic、消息、回执、在线状态、通话信令 |
| WebSocket(支持自定义 Header 的原生客户端) | wss://{host}/{api_path}v0/channels |
Upgrade Header X-MeetingIM-APIKey |
同上 |
| 产品 HTTP | https://{host}/{api_path}v1/im/* |
Authorization: Bearer <params.token> |
好友、群、收藏、搜索、工作台和设置 |
| 文件 HTTP | POST /v0/file/u/、GET /v0/file/s/{file_id} |
当前用 X-MeetingIM-Auth: token <base64_token> |
文件上传下载;禁止 Query 传 Token |
| 用户条款 | https://{host}/legal/user-terms |
无需认证、无需 API Key | 登录、注册页通过浏览器或 WebView 打开 |
| 隐私政策 | https://{host}/legal/privacy-policy |
无需认证、无需 API Key | 登录、注册及设置页通过浏览器或 WebView 打开 |
HTTP 请求统一通过 X-MeetingIM-APIKey 携带。浏览器 WebSocket 无法设置自定义 Upgrade Header 时才使用 apikey Query;日志和监控必须对该参数脱敏。产品 HTTP 还必须带 X-IM-Schema: 1 和 X-Request-ID;写操作带 Idempotency-Key。生产环境只允许 HTTPS/WSS。Token、密码、验证码、证件号不得写入 URL、普通日志、埋点或崩溃报告。
0.2.3 用户条款与隐私政策公开页面
客户端不解析或内置协议正文,只保存以下 HTTPS 地址并直接打开:
| 页面 | 通用地址 | 测试/生产域名示例 |
|---|---|---|
| 用户条款 | https://{host}/legal/user-terms |
https://api.118im.com/legal/user-terms |
| 隐私政策 | https://{host}/legal/privacy-policy |
https://api.118im.com/legal/privacy-policy |
两个页面支持 GET、HEAD,返回 text/html; charset=utf-8,不要求 X-MeetingIM-APIKey、X-IM-Schema 或用户 Token。客户端登录前即可访问;链接应使用系统浏览器或支持 HTTPS 的 WebView 打开,不要把协议正文缓存为业务数据。服务端通过 ETag 和短时公共缓存控制版本刷新,协议更新并重新发布服务后客户端仍使用同一 URL。
客户端不需要硬编码协议域名。POST /v1/im/tenants/resolve 的 data.endpoints 会同步返回:
{
"legal": {
"user_terms": "https://api.example.com/legal/user-terms",
"privacy_policy": "https://api.example.com/legal/privacy-policy"
}
}
登录、注册页点击对应文字时直接打开上述 URL,不附加 Token、API Key 或租户参数,并允许用户滚动、缩放和返回。
WebSocket Upgrade 阶段由 server/hdl_websock.go 先调用 checkAPIKey。缺少或无效时不会建立 WebSocket,直接返回 HTTP 403:
{"ctrl":{"code":403,"text":"valid API key required","ts":"2026-08-11T08:30:15.123Z"}}
客户端应把该错误视为应用配置/环境配置错误,停止自动重连并上报脱敏诊断;它不是账号密码错误,也不能通过重新登录解决。
两种连接方式的第一个请求对照如下:
| 连接方式 | 发起第一个网络请求前已有的值 | 第一个网络请求 | 第一个响应得到的值 | 下一步 |
|---|---|---|---|---|
| WebSocket(推荐) | host + api_key + tenant_code |
带 API Key Upgrade /v0/channels |
HTTP 101 和已建立的 WebSocket;不会返回新 API Key 或 sid | 发送 {hi tenant} |
| Long Polling(兼容) | host + api_key + tenant_code |
POST /v0/channels/lp |
HTTP 201 和 ctrl.params.sid |
带 sid POST {hi},再 GET 等待响应 |
现有实现对应关系:
| 源码 | 作用 |
|---|---|
keygen/keygen.go |
生成普通/Root API Key 和服务端 HMAC Salt |
server/api_key.go |
checkAPIKey 校验 API Key 格式与签名 |
server/http.go |
按 Header、Query、Form、Cookie 顺序读取 API Key |
server/hdl_websock.go |
WebSocket Upgrade 前校验 API Key,无效直接 403 |
server/hdl_longpoll.go |
创建 Long Polling Session 并返回 sid |
server/session.go |
处理 {hi} 租户绑定、{login} 和登录 Token 返回 |
现有 API Key 是应用级、非租户级,同一个普通应用 Key 可以进入不同企业;具体租户只能由后续 {hi}.tenant 决定。若将来要求每个租户独立 API Key,需要新增 Key 与租户绑定关系,不能只修改客户端配置。
0.3 多租户强制规则
- 未登录阶段只传公开的
tenant_code;客户端永远不接收、不提交数据库tenant_id。 - WebSocket 第一条消息必须是
{hi},其中tenant为企业码。服务端解析后把租户固定到 Session。 - Token 内部绑定租户。Token 租户与当前 Session 租户不一致时返回
TENANT_MISMATCH,不得自动切换。 - 切换企业必须退出登录、关闭 WebSocket、清除该租户的 Token/缓存,再重新解析企业码和握手。
- 已登录 HTTP 请求的租户和用户只从 Token Principal 获取。请求体中的
tenant_id、user_id不能覆盖身份。 - 跨租户对象统一返回
RESOURCE_NOT_FOUND或FORBIDDEN,不得暴露对象是否存在。
0.4 HTTP 请求与响应
成功响应统一为:
{
"code": 0,
"data": {},
"request_id": "01K...",
"server_time": "2026-08-11T08:30:15.123Z"
}
失败响应统一为:
{
"code": 30001,
"reason": "GROUP_MUTED",
"message": "permission denied",
"details": {},
"request_id": "01K...",
"server_time": "2026-08-11T08:30:15.123Z"
}
客户端只使用稳定的 reason 分支和本地化文案,不解析 message。ID、Topic、Seq 游标在客户端均按字符串处理;时间统一为 UTC RFC3339 毫秒。分页统一使用 limit 与不透明 cursor,返回 items、next_cursor、has_more。
资源不存在类错误统一语义(2026-09-29 修订):
- 好友申请
accept/reject在申请不存在时统一返回404/20004 RESOURCE_NOT_FOUND,两个入口语义一致。 - 群类接口(
DELETE /v1/im/groups/{topic}、POST /v1/im/groups/{topic}/leave、POST /v1/im/groups/{topic}/qrcodes、PUT /v1/im/groups/{topic}/members/me/profile、GET/PATCH /v1/im/groups/{topic}/settings等经群成员校验的路径)在群不存在时返回404/30004 RESOURCE_NOT_FOUND;群已解散时返回410/30004 GROUP_DISSOLVED;群存在但当前用户非成员仍是403/30005 PERMISSION_DENIED。 - 服务端对不存在的资源不再返回 5xx 或网关错误。
0.5 WebSocket 建连、登录和恢复顺序
0.5.1 现有服务端可直接执行的冷启动流程
读取客户端预置的 host 和普通 API Key
-> 登录页由 login_type 确定个人/企业并绑定 tenant_code;注册页由 registration_type 确定 tenant_code
-> 可选:POST /v1/im/tenants/resolve 获取公开配置或历史企业入口信息
-> 带 API Key 完成 WebSocket Upgrade
-> hi(tenant_code)
-> 收到 hi 的 201 ctrl,确认 tenant 与服务端限制
-> 有历史 Token 时 token login,否则 basic login/acc
-> 收到 login 的 200 ctrl,保存 user/token/expires
-> sub(me) 拉取本人资料和会话订阅
-> sub(fnd) 建立搜索 Topic
-> 订阅需要在线的会话并按 seq 增量拉取
-> 进入可发送状态
当前服务端已提供 /v1/im/tenants/resolve,用于服务发现、品牌配置或历史企业入口;当前登录页和注册页不把它作为必选前置校验。企业是否存在、是否可用必须在 {hi}、登录和注册链路中最终校验。即使 HTTP 解析成功,WebSocket Upgrade 返回 101 Switching Protocols 后仍必须先发送下面的 {hi} JSON 文本帧,并使用同一个 tenant_code 绑定当前 Session。
hi 请求:
{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","dev":"device-id","lang":"zh-CN","tenant":"acme"}}
login 请求使用 basic 或 token:
{"login":{"id":"login-1","scheme":"basic","secret":"base64(username:password)"}}
{"login":{"id":"login-2","scheme":"token","secret":"<上次登录响应原样返回的 token 字符串>"}}
basic.secret 是 base64(username + ":" + password)。Base64 只是协议编码,不是加密,Basic 注册和登录必须使用 HTTPS/WSS;服务端解析密码后使用 bcrypt 哈希,数据库不保存密码明文。Token 登录必须把上次 params.token 的 JSON 字符串原样填入 secret,不能再次 Base64 编码。登录成功 {ctrl} 的 params 至少包含 user、authlvl、token、expires。产品 HTTP 也使用同一个 params.token:Authorization: Bearer <params.token>,客户端不要解码或二次编码。Token 对客户端是不可解析的 opaque 字符串,只能存入系统安全存储。id 只用于匹配本次 Session 的命令响应,不是业务幂等键。
当前登录 Token 默认有效期为 14 天,不单独设置 Refresh Token 或刷新接口。每次 Basic 登录或未过期 Token 登录成功后,服务端都会返回一个新的 14 天 Token;客户端必须原子覆盖安全存储中的旧 token/expires。Token 已过期或校验失败时不能续期,必须回到账号密码登录。
hi 成功示例:
{
"ctrl": {
"id": "hi-1",
"code": 201,
"text": "created",
"params": {
"ver": "0.25",
"tenant": {"code": "acme", "name": "示例企业"},
"maxFileUploadSize": 104857600,
"callTimeout": 30,
"iceServers": []
}
}
}
客户端只使用实际响应中存在的限制字段;未知字段忽略。hi 返回非 2xx 时必须关闭连接,不得继续发送 login。
登录成功后发送:
{"sub":{"id":"sub-me-1","topic":"me","get":{"what":"desc sub tags cred"}}}
客户端按相同 id 接收 {ctrl} 和一个或多个 {meta},从 meta.desc 取得本人资料和读写游标,从 meta.sub 取得会话 Topic、访问权限、seq/read/recv 与会话摘要。不要依赖 {ctrl}、{meta} 的固定先后顺序。
随后建立查找 Topic:
{"sub":{"id":"sub-fnd-1","topic":"fnd"}}
进入具体会话时订阅并增量拉取,since 使用客户端已完整持久化的最后 seq + 1:
{"sub":{"id":"sub-topic-1","topic":"grp...","get":{"what":"desc sub data del","data":{"since":125,"limit":50},"del":{"since":1,"limit":50}}}}
向上翻页(加载更早历史)最终参数约定(后端已确认,2026-09-29):复用同一 get data,最终参数为 before(排他上界,返回 seq < before),没有 REST cursor 语义。聊天页滚动到顶部时发 data:{"before":<当前已加载最小 seq>,"limit":50}(since 可省略,默认 0)。服务端返回该锚点之前最接近 before 的 limit 条,{data} 按 seq 从大到小下发(首条即锚点前最新一条);收到 ctrl 204 no content(params.what=="data")表示已到会话顶部。data 参数全集:since(含下界)、before(排他上界)、limit、ranges。P2P 与群聊(含热缓存 fallback)语义一致。REST GET /v1/im/conversations/{topic}/messages 当前为空占位(见 4.21.1),不得用于历史分页。
订阅成功后才允许 {pub}、note read/recv 等会话操作。断线恢复必须重新执行 WebSocket Upgrade -> hi -> token login -> sub me -> 恢复 Topic,同一连接内不得跳过 hi 或切换企业码。
0.5.1.1 WebSocket 心跳与在线状态
当前 IM 长连接心跳使用 WebSocket 协议层 Ping/Pong,不使用业务 JSON 消息做保活。服务端每 15 秒发送一次 WebSocket Ping 帧(pingPeriod=15s),并期望客户端在 45 秒读超时窗口内返回 Pong(idleSessionTimeout=45s);如果没有收到 Pong,或底层读写失败,服务端会关闭当前 Session。
客户端要求:
- 使用系统 WebSocket 库时,必须确认库会自动回复 Pong;如果库不会自动回复,需要在收到 Ping 时显式发送 Pong。
- 不要把
{"note":{"what":"kp"}}当作心跳。kp/kpa/kpv只是“正在输入/正在录音/正在视频输入”等 UI 状态通知,不落库,也不能替代 WebSocket Pong。 - 连接关闭或报错后,客户端本地应立即把 IM 长连接视为离线,并按指数退避重连。重连成功后完整执行
WebSocket Upgrade -> hi -> token login -> sub me -> sub topics -> get data/del,用本地最后完整持久化的seq + 1增量补消息。 - 服务端收到任意客户端上行 JSON 消息或 WebSocket Pong 后,都会刷新在线状态。当前在线状态写入 Redis,默认 TTL 为 120 秒,用于好友在线状态、群成员在线状态等展示。
- iOS/Android 进入后台后系统可能挂起 WebSocket。后台场景以推送唤醒和前台重连补偿为主,不要求客户端在后台无限重连。
0.5.2 Long Polling 兼容流程:先取得的是 sid
如果客户端无法使用 WebSocket,可以使用现有 /v0/channels/lp。该模式确实需要“先请求取得一个值”,但取得的是短期 Session ID sid,API Key 仍然必须由客户端预先持有。
第一步,创建 Long Polling Session:
POST /v0/channels/lp HTTP/1.1
Host: im.example.com
X-MeetingIM-APIKey: <预置的普通 API Key>
Content-Length: 0
服务端返回 HTTP 201:
{"ctrl":{"code":201,"text":"created","params":{"sid":"session-id"},"ts":"2026-08-11T08:30:15.123Z"}}
第二步开始,所有 Long Polling 请求都同时携带 API Key 和 sid。发送 hi:
POST /v0/channels/lp?sid=session-id HTTP/1.1
X-MeetingIM-APIKey: <预置的普通 API Key>
Content-Type: application/json
{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"acme"}}
POST 只负责把命令送入 Session。客户端再保持一个不带 Body 的 GET 来接收下一条 {ctrl}、{meta}、{data}、{pres} 或 {info}:
GET /v0/channels/lp?sid=session-id HTTP/1.1
X-MeetingIM-APIKey: <预置的普通 API Key>
收到 hi 成功后,以相同方式 POST {login},再 GET 登录结果;登录成功后依次 POST {sub me}、{sub fnd} 和会话订阅,并持续发起下一次 GET 等待下行消息。
sid 无效或过期时服务端返回 HTTP 403 invalid or expired session,客户端必须重新创建 Long Polling Session 并从 hi 开始,不能只重发 login。Long Polling 是兼容方案,移动端和 Web 端正常网络条件下优先使用 WebSocket。
0.5.3 企业解析与产品流程
第 1.1 节的 POST /v1/im/tenants/resolve 已实现,推荐产品入口流程为:
读取预置 API Key
-> POST /v1/im/tenants/resolve 校验企业码并取得服务地址/品牌配置
-> 带同一 API Key 建立返回地址的 WebSocket
-> hi 再次绑定并校验 tenant_code
-> login -> sub me -> 恢复会话
企业解析接口只改善入口、服务发现和品牌配置,不能代替 {hi} 的租户绑定。即使 HTTP 解析成功,WebSocket 第一帧仍必须带相同 tenant_code。如果解析响应中的 endpoints 不符合当前发布环境,客户端应优先使用内置的公网环境配置,并上报脱敏诊断。
0.5.4 WSS 契约输入输出示例
本节用于 WebSocket 联调。除特别说明外:
- WebSocket 地址为
wss://{host}/{api_path}v0/channels?apikey={api_key};支持自定义 Header 的原生客户端也可用X-MeetingIM-APIKey。 - 连接建立后第一帧必须是
{hi},hi成功后才能{login}或{acc}。 - 每个上行命令都带客户端生成的
id,客户端用该id匹配{ctrl};同一连接内保持唯一即可。 - 服务端下行可能乱序到达,客户端不能假设
{ctrl}一定先于{meta}或{data}。 - 成功控制包一般为
{ctrl.code=2xx};失败控制包一般为{ctrl.code=4xx/5xx},产品稳定错误优先读ctrl.params.reason,没有时按code/text兜底。
0.5.4.1 连接、握手、登录和注册
| 场景 | 客户端发送 | 服务端返回示例 |
|---|---|---|
| WebSocket Upgrade | GET /v0/channels?apikey=AQ... |
HTTP 101 Switching Protocols;API Key 无效时 HTTP 403 |
租户握手 hi |
{"hi":{"id":"hi-1","ver":"0.25","ua":"Tinodios/1","platf":"ios","lang":"zh-CN","tenant":"default"}} |
{"ctrl":{"id":"hi-1","code":201,"text":"created","params":{"ver":"0.25","tenant":{"code":"default","name":"Default Tenant"},"maxMessageSize":4194304,"maxFileUploadSize":33554432,"maxSubscriberCount":128}}} |
| 账号密码登录 | {"login":{"id":"login-1","scheme":"basic","secret":"base64(username:password)"}} |
{"ctrl":{"id":"login-1","code":200,"text":"ok","params":{"user":"usr...","username":"alice","authlvl":"auth","token":"token...","expires":"2026-09-03T09:00:00Z"}}} |
| Token 登录 | {"login":{"id":"login-2","scheme":"token","secret":"上次登录返回的 token 原文"}} |
{"ctrl":{"id":"login-2","code":200,"text":"ok","params":{"user":"usr...","authlvl":"auth","token":"new_token...","expires":"2026-09-03T09:00:00Z"}}} |
| 邀请码注册 | {"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"alice","phone":"138...","registration_type":1,"secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"138...","resp":"AbCdE","params":{"challenge_id":"challenge-id"}}],"desc":{"public":{"fn":"alice"}}}} |
{"ctrl":{"id":"acc-1","code":200,"text":"ok","params":{"user":"usr...","username":"alice","authlvl":"auth","token":"token...","expires":"2026-09-03T09:00:00Z"}}} |
| 手机号注册 | {"acc":{"id":"acc-2","user":"new","scheme":"basic","username":"alice","phone":"15500000000","registration_type":1,"secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"15500000000","resp":"verification_token","params":{"challenge_id":"chg_123"}}],"desc":{"public":{"fn":"alice"}}}} |
{"ctrl":{"id":"acc-2","code":200,"text":"ok","params":{"user":"usr...","username":"alice","authlvl":"auth","token":"token...","expires":"2026-09-03T09:00:00Z"}}} |
| 登录失败 | {"login":{"id":"login-bad","scheme":"basic","secret":"base64(alice:wrong)"}} |
{"ctrl":{"id":"login-bad","code":401,"text":"authentication failed","params":{"reason":"AUTH_FAILED"}}} |
| 租户不匹配 | {"login":{"id":"login-tenant","scheme":"token","secret":"其他租户 token"}} |
{"ctrl":{"id":"login-tenant","code":403,"text":"tenant mismatch","params":{"reason":"TENANT_MISMATCH"}}} |
0.5.4.2 启动订阅、会话列表和增量恢复
| 场景 | 客户端发送 | 服务端返回示例 |
|---|---|---|
订阅本人 me |
{"sub":{"id":"sub-me-1","topic":"me","get":{"what":"desc sub tags cred"}}} |
{"ctrl":{"id":"sub-me-1","code":200,"text":"ok"}},随后可能收到 {"meta":{"id":"sub-me-1","topic":"me","desc":{"public":{"fn":"alice"}},"sub":[{"topic":"grp...","public":{"name":"项目群"},"seq":12,"read":10,"recv":12,"updated":"2026-08-20T09:00:00Z"}]}} |
订阅查找 fnd |
{"sub":{"id":"sub-fnd-1","topic":"fnd"}} |
{"ctrl":{"id":"sub-fnd-1","code":200,"text":"ok"}} |
| 订阅会话并拉历史 | {"sub":{"id":"sub-topic-1","topic":"grp...","get":{"what":"desc sub data del","data":{"since":1,"limit":50},"del":{"since":1,"limit":50}}}} |
{"ctrl":{"id":"sub-topic-1","code":200,"text":"ok"}},随后 {"meta":{"id":"sub-topic-1","topic":"grp...","desc":{"public":{"name":"项目群"},"seq":12},"sub":[{"user":"usr...","mode":"JRWP"}]}} 和多条 {data} |
| 断线恢复增量 | {"sub":{"id":"resume-grp-1","topic":"grp...","get":{"what":"data del","data":{"since":125,"limit":50},"del":{"since":1,"limit":50}}}} |
{"data":{"topic":"grp...","seq":125,"from":"usr...","ts":"2026-08-20T09:00:00Z","content":{"text":"恢复后的第一条"}}} |
| 只拉元数据 | {"get":{"id":"get-topic-1","topic":"grp...","what":"desc sub"}} |
{"meta":{"id":"get-topic-1","topic":"grp...","desc":{"seq":12,"public":{"name":"项目群"}},"sub":[{"user":"usr...","read":10,"recv":12}]}} |
| 离开/取消订阅 | {"leave":{"id":"leave-1","topic":"grp...","unsub":false}} |
{"ctrl":{"id":"leave-1","code":200,"text":"ok"}} |
0.5.4.3 发送消息、媒体卡片、名片和转发卡片
| 类型 | 客户端发送 | 服务端返回/下行示例 |
|---|---|---|
| 文本消息 | {"pub":{"id":"pub-text-1","topic":"p2p...","noecho":false,"head":{"mime":"application/json","x-im-schema":1,"x-im-type":"text","x-im-client-mid":"mid_1"},"content":{"text":"你好"}}} |
发送方收到 {"ctrl":{"id":"pub-text-1","code":202,"text":"accepted","params":{"seq":12}}};订阅者收到 {"data":{"topic":"p2p...","seq":12,"from":"usr...","head":{"x-im-type":"text","x-im-client-mid":"mid_1"},"content":{"text":"你好"}}} |
| 引用回复文本 | {"pub":{"id":"pub-reply-1","topic":"p2p...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"text","x-im-client-mid":"mid_reply_1","x-im-reply":{"seq":128,"type":"text","text":"今天晚上一起吃饭吗?","from":"usr..."}},"content":{"text":"好啊"}}} |
x-im-reply 必须保留在 head 中;服务端不解析业务语义,入库、实时下行和历史读取均原样返回 |
| 图片消息 | {"pub":{"id":"pub-img-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"image","x-im-client-mid":"mid_img_1"},"content":{"file_id":"file_img","thumbnail_file_id":"file_thumb","name":"a.jpg","size":12345,"mime":"image/jpeg","width":1080,"height":720}}} |
{"ctrl":{"id":"pub-img-1","code":202,"params":{"seq":13}}} |
| 语音消息 | {"pub":{"id":"pub-voice-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"voice","x-im-client-mid":"mid_voice_1"},"content":{"file_id":"file_voice","duration_ms":8000,"size":4096,"mime":"audio/aac","waveform":[1,3,5,2]}}} |
{"ctrl":{"id":"pub-voice-1","code":202,"params":{"seq":14}}} |
| 文件消息 | {"pub":{"id":"pub-file-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"file","x-im-client-mid":"mid_file_1"},"content":{"file_id":"file_pdf","name":"方案.pdf","size":102400,"mime":"application/pdf","extension":"pdf","checksum":"sha256:..."}}} |
{"ctrl":{"id":"pub-file-1","code":202,"params":{"seq":15}}} |
| 名片消息 | {"pub":{"id":"pub-card-1","topic":"p2p...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"contact_card","x-im-client-mid":"mid_card_1"},"content":{"user_id":"usr...","display_name":"张三","avatar_file_id":"","public_account":"zhangsan"}}} |
{"ctrl":{"id":"pub-card-1","code":202,"params":{"seq":16}}} |
| 合并转发卡片 | {"pub":{"id":"pub-fwd-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"forward_bundle","x-im-client-mid":"mid_fwd_1"},"content":{"bundle_id":"fbundle_1","title":"聊天记录","item_count":3,"preview":[{"from_display":"张三","summary":"你好"}]}}} |
{"ctrl":{"id":"pub-fwd-1","code":202,"params":{"seq":17}}} |
| @成员消息 | {"pub":{"id":"pub-mention-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"text","x-im-client-mid":"mid_at_1","mentions":[{"user_id":"usr...","offset":0,"length":3}]},"content":{"text":"@张三 看一下"}}} |
{"ctrl":{"id":"pub-mention-1","code":202,"params":{"seq":18}}} |
| @所有人 | {"pub":{"id":"pub-all-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"text","x-im-client-mid":"mid_all_1","mention_all":true},"content":{"text":"@所有人 开会"}}} |
无权限时 {"ctrl":{"id":"pub-all-1","code":403,"text":"permission denied","params":{"reason":"MENTION_ALL_FORBIDDEN"}}} |
重复 client_mid 重试 |
与第一次发送完全相同,复用 x-im-client-mid:"mid_1" |
目标行为:返回第一次成功的同一 seq;不得落两条消息 |
0.5.4.3.1 会话内系统提示和可点击动作消息
截图中的灰色提示、群内邀请确认说明、好友已添加提示等属于会话内系统消息,必须进入对应 P2P 或群 Topic 的消息流,占用 seq,参与历史拉取、重连补偿和多端同步。它们不是 info 实时业务事件,也不是系统通知列表记录;info 只负责触发刷新,真正需要在聊天窗口长期展示的内容必须以 {data} 下发并入库。发给被邀请人的“加入群聊”邀请卡片不写入邀请人与被邀请人的 P2P 会话,展示以 GET /v1/im/system-notifications 返回的系统通知记录为准。
客户端不得主动发送 system_notice、system_action;这两类消息只能由服务端受控身份生成。服务端下发时 data.from 可以为空,客户端以 head.x-im-from="system" 判断系统身份,不依赖真实用户头像。客户端收到未知 template 或未知 action.type 时,展示 content.text,忽略不可识别动作,不允许执行任意 URL、脚本或非白名单路由。
| 类型 | 适用场景 | 服务端下行示例 | 客户端渲染 |
|---|---|---|---|
system_notice |
“你已添加了安妮,现在可以开始聊天了”、撤回提示、群名修改、成员进出群等纯提示 | {"data":{"topic":"p2p...","seq":19,"from":"","head":{"x-im-schema":1,"x-im-type":"system_notice","x-im-template":"friend_added","x-im-from":"system"},"content":{"text":"你已添加了安妮,现在可以开始聊天了。","level":"normal","template":"friend_added","actors":[{"user_id":"usr_anne","name":"安妮"}],"actions":[]}}} |
居中灰色提示;不显示发送人头像;如 actors.click 为空则不可点 |
system_action |
审批入口、群设置入口、可点击人名或群名提示;仅在当前消息对所有接收者都可操作时下发 actions |
{"data":{"topic":"grp...","seq":20,"from":"","head":{"x-im-schema":1,"x-im-type":"system_action","x-im-template":"group_member_joined","x-im-from":"system"},"content":{"text":"安妮邀请栗子来了加入了群聊","template":"group_member_joined","actors":[{"user_id":"usr_anne","name":"安妮","click":{"type":"user_profile","target_id":"usr_anne"}},{"user_id":"usr_lizi","name":"栗子来了","click":{"type":"user_profile","target_id":"usr_lizi"}}],"actions":[]}}} |
居中灰色或业务提示样式;可点击片段按 actors.click 高亮;按钮或文字入口按 actions 渲染。若 actions 为空,不得将普通文案渲染为可点击入口 |
content 字段约定:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
text |
string | 是 | 降级展示文案;客户端可直接展示,也可按 template 重新排版 |
template |
string | 是 | 服务端稳定模板编码,例如 friend_added、group_member_joined、group_invite_confirm_enabled |
level |
string | 否 | normal、warning、danger,影响颜色和强调程度 |
actors |
array | 否 | 文案中的用户、群、文件等实体;支持局部高亮和点击 |
actions |
array | 否 | 当前消息可触发的操作入口;只允许客户端白名单动作。群邀请确认开关、全员禁言开关、成员私信权限开关等设置变更公告面向全员广播,服务端不下发仅管理员可操作的 actions;客户端不得仅凭模板名高亮入口。若历史消息或兼容字段携带 min_role,客户端必须按当前成员角色过滤,角色不足时忽略该 action |
extra |
object | 否 | 模板扩展参数;客户端不得依赖未知字段完成关键流程 |
actors[].click 和 actions[] 的 type 首版白名单:
| 类型 | 参数 | 行为 |
|---|---|---|
user_profile |
target_id |
打开用户资料页 |
group_profile |
target_id |
打开群资料页 |
open_group_invite_settings |
target_id |
打开群邀请确认设置;仅用于明确发给管理员/群主或全员都可执行的入口,不用于全员广播的开关结果公告 |
open_group_join_requests |
target_id |
打开群入群审批列表 |
open_message |
topic,seq |
定位到指定消息 |
0.5.4.4 回执、正在输入、删除和清空记录
| 场景 | 客户端发送 | 服务端返回/下行示例 |
|---|---|---|
| 送达回执 | {"note":{"topic":"p2p...","what":"recv","seq":12}} |
对端收到 {"info":{"topic":"p2p...","from":"usr...","what":"recv","seq":12}} |
| 已读回执 | {"note":{"topic":"p2p...","what":"read","seq":12}} |
对端收到 {"info":{"topic":"p2p...","from":"usr...","what":"read","seq":12}};客户端本地把该 topic read 更新为 12 |
| 正在输入 | {"note":{"topic":"p2p...","what":"kp"}} |
对端收到 {"info":{"topic":"p2p...","from":"usr...","what":"kp"}},本地超时消失 |
| 正在录音可选 | {"note":{"topic":"grp...","what":"kpa"}} |
对端收到 {"info":{"topic":"grp...","from":"usr...","what":"kpa"}},不落库 |
| 删除单条/多条消息 | {"del":{"id":"del-1","topic":"grp...","what":"msg","delseq":[{"low":12,"hi":13}],"hard":false}} |
{"ctrl":{"id":"del-1","code":200,"text":"ok","params":{"del":3}}};其他端可能收到 {"pres":{"topic":"grp...","what":"del","seq":3}} |
| 清空当前用户视图 | {"del":{"id":"del-all-1","topic":"p2p...","what":"msg","delseq":[{"low":1,"hi":126}],"hard":false}} |
{"ctrl":{"id":"del-all-1","code":200,"text":"ok","params":{"del":4}}} |
kp/kpa/kpv 只表示输入态、录音态、视频输入态等短暂 UI 状态,不能作为长连接心跳。长连接保活必须依赖 0.5.1.1 中的 WebSocket Ping/Pong。
单聊已读/已收回执协议约定(服务端已验证,2026-09-29):
- 在线下行形态:
info.topic为话题名(如p2p...),info.from为报读者,info.what为recv/read,info.seq为水位。服务端跳过上报会话自身,推送给对端及本人其他在线设备(多 Pod 部署经 Kafka 同步,各 Pod 均会下发)。 - 离线补发形态:接收方离线时收到
{"info":{"topic":"me","src":"p2p...","from":"usr...","what":"recv","seq":12}},即topic固定为me、src才是话题名。客户端必须同时兼容上述两种形态。 - 序号单调递增:服务端丢弃
seq不大于当前水位的回执(read、recv各自独立判断),read回执会同步抬升recv,恒有read ≥ recv。客户端渲染一律取服务端最大游标,禁止用本地计数覆盖服务端值。 - 断线重连恢复:
sub/get desc响应的desc中包含seq、read、recv、clear四个游标;get {"what":"sub"}也返回各成员的read/recv。重连后以这些游标恢复本地状态,不要依赖进程内存。 - 换设备登录:回执水位按「用户 × 话题」在服务端持久化,与设备无关;新设备订阅后
desc同时返回自己与对端的最新read/recv,可完整恢复对方"已读/已收"状态。发送者自身的read/recv由服务端自动推进到其最新发出的seq。
0.5.4.5 Topic 资料、个人群设置和标签
| 场景 | 客户端发送 | 服务端返回/下行示例 |
|---|---|---|
| 更新群公开资料 | {"set":{"id":"set-grp-1","topic":"grp...","desc":{"public":{"name":"新群名","avatar_file_id":"file_avatar","description":"新简介","version":2}}}} |
{"ctrl":{"id":"set-grp-1","code":200,"text":"ok"}};成员收到 {"pres":{"topic":"grp...","what":"upd"}} |
| 更新个人资料 | {"set":{"id":"set-me-1","topic":"me","desc":{"public":{"fn":"新昵称","avatar_file_id":"file_avatar"}}}} |
{"ctrl":{"id":"set-me-1","code":200,"text":"ok"}} |
| 更新会话私有设置 | {"set":{"id":"set-private-1","topic":"grp...","sub":{"user":"usr_me","private":{"pinned":true,"mute_notifications":false,"nickname":"群昵称","version":2}}}} |
{"ctrl":{"id":"set-private-1","code":200,"text":"ok"}} |
| 设置搜索标签 | {"set":{"id":"set-tags-1","topic":"me","tags":["usr:alice"]}} |
普通用户无权限时 {"ctrl":{"id":"set-tags-1","code":403,"text":"permission denied","params":{"what":"tags"}}} |
0.5.4.6 系统通知、好友和群业务下行
服务端主动下行分两类:MeetingIM 原生 Topic 消息使用 {data}、{info}、{pres} 等帧;IM 产品业务事件使用 {info} 帧下发,前端按 info.what + info.event 分发刷新。产品业务事件不占普通聊天 seq,不等同于系统通知列表记录;系统消息入口的未读数来自 im_system_notifications 和 GET /v1/im/system-notifications。
用户维度业务事件统一格式:
{
"info": {
"topic": "usr_target",
"from": "usr_actor",
"what": "im",
"event": "friend.request.accepted",
"payload": {
"user_id": "usr_actor"
},
"ts": "2026-08-25T10:00:00Z"
}
}
群维度业务事件统一格式:
{
"info": {
"topic": "grp_xxx",
"from": "usr_actor",
"what": "group",
"event": "group.member.joined",
"payload": {
"topic": "grp_xxx",
"user_id": "usr_xxx",
"member_sync": "single",
"visibility": "all_members"
},
"ts": "2026-08-25T10:00:00Z"
}
}
| 事件 | 接收方 | 服务端下行示例 | 客户端动作 |
|---|---|---|---|
| 好友申请创建 | 被申请人 | {"info":{"topic":"usr_target","from":"usr_applicant","what":"im","event":"friend.request.created","payload":{"user_id":"usr_applicant"}}} |
刷新 GET /v1/im/friend-requests 和 GET /v1/im/bootstrap 的新朋友未处理数;系统通知列表也会新增记录 |
| 好友申请通过 | 申请人、通过人本人 | {"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.accepted","payload":{"user_id":"usr_actor"}}} |
刷新新朋友列表、好友列表、P2P 会话 context;申请人当前有系统通知,通过人本人当前无系统通知 |
| 好友申请拒绝 | 申请人、拒绝人本人 | {"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.rejected","payload":{"user_id":"usr_actor"}}} |
刷新新朋友列表和 bootstrap.entry_badges;申请人当前有系统通知,拒绝人本人当前无系统通知 |
| 删除好友 | 被删除方、操作人本人 | {"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.deleted","payload":{"user_id":"usr_actor"}}} |
刷新好友列表、会话 context,重新判断输入区 can_send;被删除方当前有系统通知,操作人本人当前无系统通知 |
| 拉黑/取消拉黑 | 操作人本人 | 拉黑:{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.blocked","payload":{"user_id":"usr_actor"}}};取消拉黑:{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.unblocked","payload":{"user_id":"usr_actor"}}} |
刷新黑名单、好友列表和会话 context |
| 修改好友备注 | 操作人本人 | {"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"friend.remark.updated","payload":{"user_id":"usr_actor"}}} |
刷新好友列表和会话头显示名 |
| 消息撤回 | Topic 在线成员 | {"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.recalled","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}} |
按 topic+seq+version 重新拉取或本地标记已撤回 |
| 消息编辑 | Topic 在线成员 | {"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.edited","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}} |
按 topic+seq+version 重新拉取消息详情或修订记录 |
| 单聊消息变更脏标记 | P2P 双方在线会话 | {"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"message.mutation","payload":{"topic":"p2p...","seq":12,"version":2,"event":"message.recalled"}}} |
当前未订阅该 P2P 也要标记会话脏,并调用 message-mutations 做补偿同步 |
| 成员加入 | 群 Topic 在线成员 | {"info":{"topic":"grp...","from":"usr_actor","what":"group","event":"group.member.joined","payload":{"user_id":"usr_xxx","member_sync":"single","visibility":"all_members"}}} |
全体成员可见;调用 GET /v1/im/groups/{topic}/members/{user_id} 刷新单个成员。加入事件同时写入群消息,离线后可按 Topic seq 补拉 |
| 成员退出/移除、角色或禁言变化 | 群主、管理员和被操作人 | {"info":{"topic":"grp...","from":"usr_actor","what":"group","event":"group.member.left","payload":{"user_id":"usr_xxx","member_sync":"remove","visibility":"admins_and_affected"}}} |
普通成员不可见;single 拉取单成员,remove 本地删除,full 重新分页拉取全部成员 |
| 其他群业务变化 | 按具体事件授权范围 | info.what="group" |
刷新群详情、入群申请、公告或治理权限 |
| 会议变化 | 会议参会人 | {"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"meeting.updated","payload":{"meeting_id":"mtg_1","version":2,"state":"scheduled"}}} |
刷新会议列表/详情;除操作者外当前会写系统通知 |
| 安全风险确认 | 操作人本人 | {"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"security.risk.continue","payload":{"risk_challenge_id":"risk_1","decision":"continue"}}} |
刷新安全验证页面;当前会写系统通知 |
业务事件写入 outbox 后会立即尝试投递;Hub 不可用或队列满时保持 pending,后台 poller 按在线租户重试。registration.invite_used 和 security.password_reset 是服务端内部消费事件,不作为普通业务事件下发给 App。
0.5.4.7 WebRTC 通话信令
| 场景 | 客户端发送 | 服务端返回/下行示例 |
|---|---|---|
| 发起语音通话 | {"pub":{"id":"call-pub-1","topic":"p2p...","head":{"mime":"application/json","webrtc":"started","x-im-type":"call","x-im-client-mid":"mid_call_1"},"content":{"callId":"call_1","callType":"audio","audio":true,"video":false}}} |
{"ctrl":{"id":"call-pub-1","code":202,"text":"accepted","params":{"seq":30}}};被叫收到同一 {data seq=30} |
| 被叫响铃 | {"note":{"topic":"p2p...","what":"call","event":"ringing","seq":30,"payload":{"callId":"call_1"}}} |
主叫收到 {"info":{"topic":"p2p...","what":"call","event":"ringing","seq":30,"payload":{"callId":"call_1"}}} |
| 被叫接听 | {"note":{"topic":"p2p...","what":"call","event":"accept","seq":30,"payload":{"callId":"call_1"}}} |
双方收到通话状态 accept;原消息可被服务端替换为 head.webrtc:"accepted" |
| SDP offer | {"note":{"topic":"p2p...","what":"call","event":"offer","seq":30,"payload":{"callId":"call_1","sdp":"v=0..."}}} |
对端收到 {"info":{"topic":"p2p...","what":"call","event":"offer","seq":30,"payload":{"callId":"call_1","sdp":"v=0..."}}} |
| SDP answer | {"note":{"topic":"p2p...","what":"call","event":"answer","seq":30,"payload":{"callId":"call_1","sdp":"v=0..."}}} |
对端收到 event:"answer" |
| ICE candidate | {"note":{"topic":"p2p...","what":"call","event":"ice-candidate","seq":30,"payload":{"callId":"call_1","candidate":{"candidate":"candidate:...","sdpMid":"0","sdpMLineIndex":0}}}} |
对端收到 event:"ice-candidate" |
| 挂断/取消 | {"note":{"topic":"p2p...","what":"call","event":"hang-up","seq":30,"payload":{"callId":"call_1","reason":"cancelled"}}} |
服务端替换原通话消息:{"data":{"topic":"p2p...","seq":30,"head":{"replace":":30","webrtc":"missed","webrtc-duration":0,"x-im-type":"call"},"content":{"callId":"call_1","callType":"audio","status":"missed"}}} |
| 异常事件 | {"note":{"topic":"p2p...","what":"call","event":"media-state","seq":30,"payload":{"callId":"call_1"}}} |
当前未开放时返回 {"ctrl":{"code":400,"text":"malformed","params":{"reason":"CALL_EVENT_UNSUPPORTED"}}} 或直接忽略,客户端不要提前依赖 |
0.5.4.8 WSS 错误、重试和客户端处理
| 场景 | 服务端返回示例 | 客户端处理 |
|---|---|---|
| API Key 无效 | Upgrade HTTP 403 {"ctrl":{"code":403,"text":"valid API key required"}} |
停止重连,上报环境配置错误 |
未先 hi 就登录 |
{"ctrl":{"id":"login-1","code":400,"text":"tenant required","params":{"reason":"TENANT_REQUIRED"}}} |
关闭连接,从 Upgrade + hi 重来 |
| 登录失败 | {"ctrl":{"id":"login-1","code":401,"text":"authentication failed","params":{"reason":"AUTH_FAILED"}}} |
清密码,保留账号,显示统一账号或密码错误 |
| Token 过期 | {"ctrl":{"id":"login-token-1","code":401,"text":"authentication failed","params":{"reason":"TOKEN_EXPIRED"}}} |
清当前租户 token,回登录页 |
| 无 Topic 权限 | {"ctrl":{"id":"pub-1","code":403,"text":"permission denied","params":{"reason":"MESSAGE_NOT_ALLOWED"}}} |
发送气泡置为失败,不伪装已发送 |
| 消息过大 | {"ctrl":{"id":"pub-1","code":413,"text":"message too large","params":{"reason":"MESSAGE_TOO_LARGE","max_size":4194304}}} |
禁止重试,提示压缩或拆分 |
| 断线 | WebSocket close | 指数退避重连,重新 hi -> token login -> sub me -> sub topics,按最后完整 seq + 1 增量拉取 |
0.5.4.9 极光离线 Push 与重连补偿
离线 Push 只承担提醒和唤醒 App,不作为消息正文或业务状态真源。客户端收到极光 Push 后,必须重新登录 IM,通过 topic + seq、call_id 或系统通知 ID 拉取服务端状态。普通聊天、群 @、系统通知、好友/群审批、音视频来电都复用同一套 extras 分发协议。
客户端侧要求:
- App 集成 JPush SDK,初始化使用极光
AppKey,不接触MasterSecret。 - 当前测试环境使用
registration_id模式,服务端配置jpush_use_alias=0,不依赖客户端 alias;alias 绑定可保留但不是投递依据。 - SDK 返回
registration_id后调用PUT /v1/im/devices/{device_id}/push-token上报,provider固定传jpush。device_id必须稳定,同一台设备不要每次启动变化。 - 切换账号、切换租户、清登录态后必须重新上报 Push Token;客户端本地的
user_id/token/registration_id去重缓存必须按tenant_code + user_id + device_id + registration_id隔离,避免把设备继续绑在旧账号上。 - 若后续切回 alias 模式,登录 IM 后将极光 alias 绑定为当前
user_id的安全别名:user_id.replace("-", ".").replace("_", "|"),例如usrd6Ag5opa-ww绑定为usrd6Ag5opa.ww;同一用户多设备允许绑定同一个 alias。 - 点击 Push 后按
extras.type跳转,再按服务端数据补偿,不能只凭 Push payload 渲染最终状态。
服务端侧要求:
jpush_master_secret只保存在后台配置im_jpush分组,IM Server 通过内部配置接口读取并调用极光 REST API。- 当前测试环境按
registration_id投递:服务端从im_device_push_tokens按目标user_id + provider=jpush查询设备 token,并调用极光audience.registration_id。 - 用户在线时优先 WebSocket 实时投递;用户离线、后台或不可投递时生成 Push 任务。
- 万人大群实时 fanout 不靠 Push;离线只对需要提醒的成员生成极光 Push,客户端重连后按 Topic seq 拉取补偿。
- Push payload 默认不带敏感正文,通知栏文案按用户隐私设置降级。
- iOS 测试环境固定使用
options.apns_production=false;客户端包也必须是开发 APNs 环境。TestFlight/App Store 生产包需要后端与客户端同时切到生产 APNs。
极光 Push extras 统一格式:
{
"type": "im_message",
"tenant_code": "default",
"topic": "p2p...",
"seq": "128",
"sender_id": "usr_sender",
"message_type": "text"
}
extras.type |
必填字段 | 客户端动作 |
|---|---|---|
im_message |
tenant_code,topic,seq |
打开会话,按本地最后 seq 补齐消息并定位。 |
group_mention |
tenant_code,topic,seq,sender_id |
打开群会话并定位被 @ 消息。 |
call_invite |
tenant_code,topic,seq,call_id,call_type,caller_id,caller_display,expires_at |
打开来电页前先登录 IM 并确认通话仍为有效来电。 |
call_missed |
tenant_code,topic,call_id |
打开通话记录或对应会话。 |
system_notification |
tenant_code,notification_id |
打开系统消息列表并刷新详情。 |
friend_request |
tenant_code,request_id |
打开新朋友列表并刷新未处理数。 |
group_request |
tenant_code,topic,request_id |
打开群审批入口并刷新列表。 |
普通离线消息极光 payload 示例:
{
"platform": "ios",
"audience": {
"registration_id": ["161a3797c95ee4a8bd3"]
},
"notification": {
"ios": {
"alert": {
"title": "MeetingIM",
"body": "你收到一条新消息"
},
"sound": "default",
"badge": "+1",
"extras": {
"type": "im_message",
"tenant_code": "default",
"topic": "p2p...",
"seq": "128"
}
}
},
"options": {
"time_to_live": 86400,
"apns_production": false,
"classification": 1
}
}
音视频来电极光 payload 示例:
{
"platform": "ios",
"audience": {
"registration_id": ["161a3797c95ee4a8bd3"]
},
"notification": {
"ios": {
"alert": {
"title": "语音通话",
"body": "张三邀请你语音通话"
},
"sound": "default",
"badge": "+1",
"category": "im_call",
"content-available": 1,
"extras": {
"type": "call_invite",
"tenant_code": "default",
"topic": "p2p...",
"seq": "30",
"call_id": "call_01K...",
"call_type": "audio",
"caller_id": "usr_caller",
"caller_display": "张三",
"expires_at": "2026-09-18T10:00:30Z"
}
}
},
"options": {
"time_to_live": 30,
"apns_production": false,
"classification": 1
}
}
来电推送必须走普通 APNs/极光 alert 通知,不使用 PushKit VoIP topic;iOS notification.ios.category 固定为 im_call,用于客户端通知按钮,同时带 content-available: 1 唤醒后台 App 拉取 topic/seq 并启动 Live Activity。time_to_live 和 expires_at 以 30 秒左右短有效期为准,过期后服务端不再投递。
0.5.5 Long Polling 与文件基础示例
0.5.5.1 WebSocket 与文件基础接口
| 接口 | 输入示例 | 成功输出示例 |
|---|---|---|
GET /v0/channels?apikey={api_key} |
WebSocket Upgrade 后首帧 {"hi":{"id":"hi-1","ver":"0.25","ua":"ios/1.0","lang":"zh-CN","tenant":"default"}} |
{"ctrl":{"id":"hi-1","code":201,"text":"created","params":{"tenant":{"code":"default","name":"Default Tenant"},"ver":"0.25","maxMessageSize":4194304}}} |
WebSocket login basic |
{"login":{"id":"login-1","scheme":"basic","secret":"base64(username:password)"}} |
{"ctrl":{"id":"login-1","code":200,"params":{"user":"usr...","token":"token...","expires":"2026-09-03T09:00:00Z","authlvl":"auth"}}} |
WebSocket acc new |
{"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"alice","phone":"15500000000","registration_type":1,"secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"15500000000","resp":"verification_token","params":{"challenge_id":"chg_123"}}]}} |
{"ctrl":{"id":"acc-1","code":200,"params":{"user":"usr...","username":"alice","token":"token...","expires":"2026-09-03T09:00:00Z"}}} |
POST /v0/channels/lp |
Long Polling 首包 {"hi":{"id":"hi-1","ver":"0.25","tenant":"default"}} |
{"ctrl":{"id":"hi-1","code":201,"params":{"sid":"lp_sid_123"}}} |
POST /v0/file/u/ |
Multipart 文件;Header X-MeetingIM-Auth: token <base64_token>;聊天附件必须带 topic=<消息所在会话 topic>(p2p.../grp...),可选 purpose=chat_file、id=<幂等ID> |
{"ctrl":{"code":200,"params":{"expires":"2026-10-13T06:21:20Z","file_id":"s6yhMyiOfaU","url":"/v0/file/s/s6yhMyiOfaU.txt"}}} |
GET /v0/file/s/{file_id} |
Header X-MeetingIM-Auth: token <base64_token>,可带 Range: bytes=0-;URL 可带文件扩展名(服务端自动剥离) |
对象存储模式下鉴权通过后返回 302 到短期签名 URL;客户端跟随 Location 获取文件 |
文件上传/下载鉴权与回收规则(已实现,客户端必须遵守):
- 上传聊天附件(图片/视频/语音/文件)必须携带
topic=<消息将要发送到的会话 topic>。服务端校验上传者是该p2p/grp会话的订阅成员(modewant/modegiven含读权限),不是成员返回403 permission denied;校验通过则在服务端建立「文件-会话」引用,该会话全体成员凭有效 Token 即可下载。 - 不带
topic的上传只有上传者本人可下载(可用于头像等场景)。任何文件若一直未被任何会话/消息引用,服务端在创建 1 小时后由后台任务物理回收;上传后必须及时发送消息或携带topic,否则对端将拿到 404。 - 下载放行范围:上传者本人、文件所属会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用。非成员返回
403 permission denied(即FILE_FORBIDDEN),客户端不自动重试。
0.6 Topic 与产品消息
核心命令:sub 订阅、pub 发送、get 拉取、set 更新 Topic、leave 离开、del 删除、note 发送回执/临时状态。发送成功为 {ctrl code:202, params:{seq}},服务端下行消息为 {data}。
所有产品消息在 pub.head 中携带:
{
"pub": {
"id": "pub-1",
"topic": "p2p...",
"noecho": false,
"head": {
"x-im-schema": 1,
"x-im-type": "text",
"x-im-client-mid": "01K...",
"mentions": []
},
"content": {"text":"你好"}
}
}
x-im-client-mid 由客户端生成 ULID/UUID,作用域为“租户 + 发送人”,断线重试必须复用;服务端目标实现应返回第一次写入的同一 seq。未知 x-im-type 显示“暂不支持的消息”,不得崩溃或删除消息。
引用回复使用 head.x-im-reply 承载客户端生成的轻量快照,字段为 seq:number、type:string、text:string,可选 from:string。该对象必须放在 head,不能放入 content;服务端只负责把未知 head 字段随消息存储、实时转发、历史拉取和 HTTP 查询一起原样返回,不对被引用消息做二次查询或语义校验。客户端兼容历史服务端将 x-im-reply 作为 JSON 字符串返回的情况,但新版本客户端发送时必须使用 JSON 对象。
消息类型首版固定为:text、image、video、voice、file、contact_card、link、forward_bundle、call、system_notice、system_action、system_event。自定义表情启用后追加 sticker。媒体内容只保存 file_id、文件元数据和缩略图 ID,不保存可长期绕过鉴权的公共 URL。system_notice 和 system_action 只能由服务端生成,普通用户构造后发送必须被服务端拒绝。
0.7 HTTP 幂等、重试和安全
- 创建、提交、同意、拒绝、删除、转让、收藏、转发等写请求必须携带
Idempotency-Key。 - GET、断线同步和同幂等键写请求可自动重试;密码、验证码和风控确认只按接口返回的
retry_after_seconds重试。 401 TOKEN_EXPIRED不执行刷新接口,立即清理失效 Token 并返回账号密码登录;其他401也不得无限重试。403不重试,429按服务端退避。- 服务端必须在最终写入前再次校验好友关系、群角色、禁言、消息归属、文件 ACL、租户配额和风控,不能信任前端按钮状态。
- 普通用户禁止向系统 Topic 发布;系统通知必须由服务端受控身份生成。
1. 账号注册、登录与身份安全
1.1 企业码登录.png
状态:当前客户端不再使用独立企业码进入页;企业码在登录页和注册页内填写,由登录/注册链路校验。企业解析接口保留给服务发现、品牌配置和历史入口。
当前系统的执行方式:
- 前端动作:登录页由“企业用户/个人用户”入口决定租户上下文,basic 登录必须提交
login_type,0表示企业登录、1表示个人登录;个人用户入口内部使用tenant_code=default,企业用户入口必须填写企业码且不得为default。注册页由registration_type明确区分个人注册和企业注册,不能通过企业码是否为空推断。企业码只去掉首尾空格,最长 64 字符,不擅自改变大小写。default是系统保留的个人租户码,不允许用户手动输入。 - 客户端从构建/环境配置读取
host和普通 API Key,直接使用wss://{host}/{api_path}v0/channels?apikey={api_key}建立 WebSocket;当前登录/注册主流程不要求先调用POST /v1/im/tenants/resolve。 - Upgrade 成功后第一帧发送
{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"登录/注册页确定的 tenant_code"}}。 - 服务端通过
im_tenant.code查询企业;成功返回{ctrl code:201}和params.tenant{code,name},同时固定 Session 的内部tenant_id。客户端保存服务端返回的标准tenant.code/name,再进入 1.2 登录。 - 当前失败表现:企业码超过 64 字符返回 400,企业不存在按存储错误返回,企业停用返回 403。登录/注册页统一展示“企业不存在或不可用”。
企业解析接口:
POST /v1/im/tenants/resolve未登录可调用,但必须带X-MeetingIM-APIKey和X-IM-Schema: 1;Header 传X-Tenant-Code: acme,Body 可传{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}。- 成功数据:
tenant{code,name,tenant_desc,branding}、endpoints{websocket,api,file}、auth_methods、password_policy、app_update。不得返回tenant_id或tenant_ticket。 - 后续:该接口仅用于服务发现、品牌配置或历史企业入口;当前登录页和注册页不依赖它完成主流程。调用方即使解析成功,仍然必须发送 0.5 节的
hi(tenant)做最终租户绑定。 - 目标失败协议:企业不存在、停用或不允许登录统一返回
TENANT_UNAVAILABLE;客户端不得区分不存在和停用。网络失败保留输入并允许重试。 - 个人登录不通过企业码入口解析
default,由个人用户入口提交login_type=1并内部使用tenant_code=default;企业登录提交login_type=0,如果企业码为default,服务端拒绝并统一提示“企业不存在或不可用”。个人注册必须提交registration_type=1,由该字段进入default注册链路。详见 IM-个人无企业码注册前后端对接说明.md。
成功响应示例:
{
"code": 0,
"data": {
"tenant": {"code": "acme", "name": "示例企业", "tenant_desc": null, "branding": {}},
"endpoints": {
"websocket": "wss://im.example.com/v0/channels?apikey=AQ...",
"api": "https://im.example.com/v1/im",
"file": "https://im.example.com/v0/file"
},
"auth_methods": ["password", "sms"],
"password_policy": {"min_length": 8, "max_length": 64},
"app_update": {"type": "none"}
},
"request_id": "01K...",
"server_time": "2026-08-11T08:30:15.123Z"
}
1.1.1 企业注册-登录入口.png
状态:已实现。
- 页面入口:企业码登录页右上角展示“企业注册”,底部展示“遇到问题?”。两个入口均为未登录入口,但必须使用客户端预置的普通 API Key,不需要用户 Token。
- 点击“企业注册”:进入 1.1.2 企业资料提交流程。客户端保留当前已输入的
tenant_code作为可选线索,但不得把它当成已解析企业身份。 - 点击“遇到问题?”:进入 1.1.4 问题反馈页。若用户已输入企业码,提交反馈时带
tenant_code_hint;未输入时可为空。 - 埋点和日志:可记录入口点击事件,但不得记录 API Key、完整联系方式、证照图片 URL 或上传文件真实地址。
- 入口展示:入口文案必须避开系统状态栏、安全区域和键盘遮挡;网络不可用时仍允许进入填写页,提交时再提示网络错误。
1.1.2 企业注册-填写注册信息.png
状态:已实现。
- 页面步骤:展示“填写企业信息、确定信息、提交审核”三步,当前处于第 1 步。提交前客户端在本页完成格式校验,服务端仍做最终校验。
- 必填字段:
credit_code18 位统一社会信用代码,客户端仅去掉首尾空格并转大写展示;提交值必须匹配^[0-9A-Z]{18}$。不得提交中文、空格或短码。 - 可选字段:
license_file_id营业执照图片文件 ID,enterprise_email企业邮箱。邮箱用于接收审核结果和后续通知,填写时必须符合邮箱格式。 - 扫描识别:扫码或 OCR 只能作为填充辅助,提交前仍以输入框中的
credit_code为准;识别失败不阻塞手动填写。 - 图片上传:营业执照先调用公开用途受限上传接口,
purpose=enterprise_license,支持jpg/png/webp/pdf,单文件不超过 10MB。服务端通过当前 media handler 存储对象,生产环境必须配置osshandler;上传成功返回file_id后再提交申请;未上传营业执照时license_file_id可为空。 - 提交接口:
POST /v1/im/tenant-applications,未登录可调用,必须带X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID和Idempotency-Key。 - 请求体:
{
"credit_code": "91310000MA1K000000",
"enterprise_email": "admin@example.com",
"license_file_id": "file_01K...",
"tenant_code_hint": "acme",
"client_context": {
"platform": "android",
"app_version": "1.0.0",
"device_model": "Pixel 8"
}
}
- 成功响应:服务端创建企业注册申请单,状态为
received或pending_review。客户端进入 1.1.3,不得重复提交同一个幂等键。
{
"application_id": "entapp_01K...",
"status": "received",
"estimated_review_hours": 24,
"submitted_at": "2026-08-24T08:30:15.123Z"
}
- 失败处理:
INVALID_CREDIT_CODE显示统一社会信用代码格式错误;INVALID_EMAIL定位邮箱输入;FILE_UNAVAILABLE允许重新上传;APPLICATION_DUPLICATED提示已有申请正在审核;RATE_LIMITED按服务端退避时间禁用提交按钮。
1.1.3 企业注册-提交成功提示.png
状态:已实现。
- 触发条件:1.1.2 提交接口返回成功后,在当前页展示 Toast:“提交成功,我们会尽快联系你”。
- 状态保存:客户端保存
application_id,status,submitted_at,estimated_review_hours到本地短期缓存,用于返回页面时展示“已提交”状态。该缓存不代表审核结果,最终状态以服务端和邮件/通知为准。 - 防重复:提交成功后禁用提交按钮;用户返回后再次进入企业注册页,可显示已提交提示并允许重新填写新申请,但必须使用新的
Idempotency-Key。 - 审核通知:审核结果默认通过
enterprise_email触达;如果后续后台接入站内通知或短信,客户端只按服务端下发的公开状态展示,不自行推断审核通过。 - 文案规则:页面底部审核时长应使用服务端返回的
estimated_review_hours或后台配置,不写死。
1.1.4 企业注册-问题反馈.png
状态:已实现。
- 使用场景:企业码不可用、企业注册资料提交异常、审核通知未收到、登录入口无法进入等未登录阶段问题。
- 输入字段:
description必填,长度 10~1000 字符;contact可选,用于客服回访;tenant_code_hint可选,来自用户输入;screenshot_file_ids最多 9 个。 - 截图上传:截图先调用公开用途受限上传接口,
purpose=tenant_entry_feedback,支持jpg/png/webp,单张不超过 10MB。服务端通过当前 media handler 存储对象,生产环境必须配置osshandler;上传失败时允许单张重试或删除。 - 提交接口:
POST /v1/im/tenant-entry-feedback,未登录可调用,必须带X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID和Idempotency-Key。 - 请求体:
{
"description": "企业码 acme 提示不可用,无法进入企业。",
"contact": "user@example.com",
"tenant_code_hint": "acme",
"screenshot_file_ids": ["file_01K..."],
"client_context": {
"platform": "ios",
"app_version": "1.0.0",
"network": "wifi"
}
}
- 成功响应:客户端展示“已收到反馈,我们会尽快处理”,并清空草稿和已提交截图引用。
{
"feedback_id": "entryfb_01K...",
"status": "received",
"submitted_at": "2026-08-24T08:35:15.123Z"
}
- 失败处理:
DESCRIPTION_REQUIRED或DESCRIPTION_TOO_LONG定位描述框;TOO_MANY_FILES提示最多 9 张;FILE_UNAVAILABLE提示重新上传失效图片;RATE_LIMITED禁用提交并展示稍后重试。
1.2 登录页面.png
状态:基本账号密码登录已实现;WS Token 可复用于产品 HTTP。
- 前端动作:企业码、账号、密码和协议勾选在同一个登录页完成;个人用户可不填企业码,企业用户填写企业码。账号、密码非空且已勾选协议后启用登录;密码仅驻留当前页面内存。
- 协议:点击登录后先按页面企业码确定
tenant_code并发送 WebSocket{hi};hi成功后发送{login scheme:"basic"},secret=base64(username:password)。Base64 不是加密,必须通过 WSS 传输。同一次登录不得先登录再切换企业码。 - 成功:读取
{ctrl params.user/token/expires/authlvl},Token 存 Keychain/Keystore;立即sub me,再加载会话。前端不解码 Token,也不再次 Base64 编码。 - 产品 HTTP:使用同一个
params.token作为Authorization: Bearer <params.token>;所有/v1/im/*HTTP 请求仍必须带X-MeetingIM-APIKey、X-IM-Schema: 1,写请求再带Idempotency-Key。 - Token:当前复用 MeetingIM 单一登录 Token,默认有效期 14 天,不使用 Refresh Token。有效 Token 登录成功会签发新的 14 天 Token,客户端覆盖旧值;过期后重新账号密码登录。Token 已绑定
tenant_id/user_id/auth_version,修改密码、封禁账号后旧 Token 应失效。 - 密码:服务端使用
bcrypt.DefaultCost=10生成单向哈希,im_auth.secret只保存哈希;客户端和服务端不得记录密码或完整 Basicsecret。 - 错误:企业不存在、停用或不允许登录统一显示“企业不存在或不可用”;
AUTH_FAILED统一显示账号或密码错误;TENANT_MISMATCH清 Token 并留在登录页重新选择个人/企业身份;ACCOUNT_DISABLED、REALNAME_REQUIRED、YOUTH_RESTRICTED按专用页面处理。
登录成功示例:
{
"ctrl": {
"id": "login-1",
"code": 200,
"text": "ok",
"params": {
"user": "usr...",
"authlvl": "auth",
"token": "base64-opaque-token",
"expires": "2026-08-11T10:30:15.123Z"
}
}
}
失败仍是同一 {ctrl} 结构,使用 code 和改造后的稳定 params.reason 分支;客户端不得用 text 做程序判断。
1.3 登录页面-密码错误.png
状态:已实现基础错误,错误码和限流需改造。
- 触发:
login返回{ctrl code:401}或产品错误AUTH_FAILED。 - 前端:保留账号、清空密码、聚焦密码框;统一显示“账号或密码错误”,不得显示账号是否存在。
- 服务端:按
tenant + account + device/IP限流,连续失败返回AUTH_RATE_LIMITED和retry_after_seconds;日志仅记录账号哈希和 Request ID。 - 验收:同一不存在账号与错误密码在响应码、耗时区间、前端文案上保持一致。
1.4 注册页面.png
状态:注册原型和服务端注册分流均已完成;邀请码不再使用独立输入框,与短信验证码共用同一输入框。
- 页面规则:用户名和手机号是独立输入。用户名非空时必须保存为
im_users.username;手机号非空时必须保存为规范化后的im_users.phone。两个字段同时为空时拒绝注册。 - 注册类型规则:注册页必须提交 int 字段
registration_type,0表示企业注册、1表示个人注册;后端按该字段区分个人注册和企业注册,不能通过企业码是否为空推断。registration_type=1时企业码为空,服务端内部使用tenant_code=default;registration_type=0时企业码必填。前端不做企业码拦截,不单独调用/v1/im/tenants/resolve,获取验证码和提交注册时带上registration_type=0与用户输入的企业码,由后端在注册链路中校验存在且可用;失败统一显示“企业不存在或不可用”。 - 用户名格式:普通用户名必须匹配
^[a-z0-9]{1,10}$,即 1~10 位小写英文字母或数字;允许纯数字,不允许大写、中文、空格、点、下划线和其他符号。前端限制输入,服务端按原始值再次校验,失败返回USERNAME_INVALID_FORMAT。 - 统一验证码输入:输入 5 位大小写英文字母时按邀请码处理;输入 6 位数字时按短信验证码处理。其他格式直接提示输入无效,客户端不得自动转换邀请码大小写。
- 邀请码格式:必须匹配
^[A-Za-z]{5}$,服务端在当前租户内按明文精确查询。邀请码有效时允许注册并建立邀请码与新用户的推荐使用关系。 - 邀请码可复用:同一有效邀请码可以供多个用户注册,使用后仍保持
active;服务端为每个注册用户记录邀请来源。只有到期或管理员撤销后才不可继续使用。 - 邀请码主数据位于 HotGo 控制库并按
tenant_id隔离;注册使用明细位于当前租户业务库。客户端契约不受分库影响,仍只提交完整 5 位邀请码。 - 旧版独立
invite_code字段仅保留协议兼容,新客户端不得再展示独立邀请码输入框。 - 找回提示:用户名方式提交注册前,客户端明确提示“未绑定手机号时无法自助找回密码,忘记密码需联系企业管理员重置”。
- 手机号方式:前端只接受 11 位数字。调用
POST /v1/im/auth/verification-codes,Body{"scene":"register","phone":"138..."};后端补+86后发送,成功返回challenge_id、expires_in_seconds、resend_after_seconds。当前测试环境短信供应商未接入,注册、找回密码和敏感操作验证码固定填写123456,用它调用 verify 接口换取一次性verification_token或action_token;生产接入短信供应商后不得继续使用固定验证码。 - 短信验证码方式:校验成功后返回一次性
verification_token,注册成功时建立已完成的tel credential。 - 邀请码方式:校验成功返回
verification_mode=invite_code;注册时把原始邀请码放在cred[].resp,不提交独立invite_code。该方式保存手机号但不建立已完成的tel credential,同时写入邀请码推荐使用关系。 - 提交注册:
username非空时,secret使用username:password;仅手机号注册且 username 为空时,客户端仍使用规范化手机号与密码生成secret,服务端创建后返回随机 username。 - 服务端判定:
username非空时校验^[a-z0-9]{1,10}$并要求secret登录名与 username 一致;username为空且phone非空时要求secret登录名与规范化手机号一致,随后生成随机 username。普通用户名允许纯数字,因此不能只根据secret是否为 11 位数字推断注册方式。 - 账户昵称:注册创建资料时,昵称暂时默认等于最终 username。填写用户名时昵称等于该用户名;仅手机号注册时昵称等于服务端随机 username,不使用手机号作为昵称。
- 禁止客户端提交
tags;手机号方式始终写入im_users.phone,只有短信验证成功时才建立已完成的电话凭证。
用户名方式:
{"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"alice","phone":"138...","registration_type":1,"secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"138...","resp":"AbCdE","params":{"challenge_id":"challenge-id"}}]}}
手机号验证方式:
{"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"alice","phone":"138...","registration_type":1,"secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"138...","resp":"verification_token","params":{"challenge_id":"challenge-id"}}]}}
仅手机号注册方式:
{"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"","phone":"138...","registration_type":1,"secret":"base64(+86138...:password)","login":true,"cred":[{"meth":"tel","val":"138...","resp":"verification_token","params":{"challenge_id":"challenge-id"}}]}}
- 成功:返回新
user、最终username和登录 Token;客户端保存params.token,随后执行sub me和会话恢复。创建用户前原子校验im_tenant_config.max_users,超限返回TENANT_USER_QUOTA_EXCEEDED。 - 安全:用户名、手机号分别在租户内唯一;验证码票据单次消费,邀请码在有效期内可重复使用;密码按服务端策略校验并使用强哈希保存。
1.5 注册页面-倒计时.png
状态:新增。
- 倒计时以 1.4 返回的
resend_after_seconds为准,并结合server_time计算,不能只依赖本地固定 60 秒。 - 倒计时内前端禁用按钮;即使绕过按钮重复调用,服务端返回
VERIFICATION_RATE_LIMITED和剩余秒数。 - App 重进页面可使用内存/安全临时存储中的
challenge_id恢复;超过expires_in_seconds必须重新发送,旧验证码失效。
1.6 忘记密码.png
状态:新增。
- 适用范围:仅
im_users.phone非空的账号支持短信自助找回。phone=NULL的用户名账号不提供自助找回,客户端提示联系当前企业管理员重置密码。 - 发码:
POST /v1/im/auth/verification-codes,Body{"scene":"reset_password","phone":"..."}。无论账号是否存在都返回相同受理响应。 - 校验:
POST /v1/im/auth/password-reset/challenges/{challenge_id}/verify,Body{"code":"123456"},成功返回短时、单次使用的reset_token。 - 重置:
POST /v1/im/auth/password-reset/confirm,Body{"reset_token":"...","new_password":"..."},带幂等键。 - 凭证补建:只要账号的
phone非空,成功完成reset_password短信挑战即可证明当前持有该号码;即使注册时未建立tel credential,本次成功后也可补建完成状态的电话凭证。 - 成功:服务端更新密码、递增
auth_version,使该用户全部旧登录 Token 失效并撤销在线 Session;客户端清理本租户凭证并返回登录页。 - 失败:
PASSWORD_POLICY_VIOLATION返回规则明细;验证码错误不返回账号存在性,达到上限后冻结 challenge。 - 管理员路径:无手机号账号只能由当前租户管理员调用
POST /admin/v1/tenant/users/{user_id}/reset-password重置;不得根据用户名跨租户查找或操作。
1.7 身份验证(安卓).png
状态:新增。
- 敏感操作先调用
POST /v1/im/security/challenges;开启青少年模式设置独立密码时 Body 为{"scene":"set_independent_password","channel":"sms"},关闭青少年模式时 Body 为{"scene":"close_youth_mode","channel":"sms"};群主转让使用{"scene":"group_owner_transfer","channel":"sms"};解散群聊使用{"scene":"group_dissolve","channel":"sms"};注销账号使用{"scene":"delete_account","channel":"sms"}。手机号由登录身份读取,客户端不提交目标手机号。 - 发码/校验:服务端返回
challenge_id和脱敏手机号;提交POST /v1/im/security/challenges/{id}/verify,Body{"code":"..."}。 - 重复发码:同一用户、同一手机号、同一敏感场景存在未过期 challenge 且仍在
resend_after_seconds冷却期内时,服务端返回 HTTP429 VERIFICATION_RATE_LIMITED,details带challenge_id和retry_after_seconds。客户端应复用该challenge_id继续倒计时,不要当成接口不可达或重新登录。 - 小时限流:超过一小时创建次数上限时同样返回 HTTP
429 VERIFICATION_RATE_LIMITED,details.retry_after_seconds可能大于 60 秒且不一定带challenge_id。客户端只展示倒计时,倒计时结束后允许用户重新获取验证码。 - 成功返回
action_token、action、expires_at;Token 只能用于指定动作且单次消费。设置独立密码接口通过 Bodyaction_token携带;关闭青少年模式、群主转让、解散群聊接口通过 HeaderX-Action-Token携带。 - 错误:
VERIFICATION_RATE_LIMITED、CHALLENGE_EXPIRED、CODE_INVALID、TOO_MANY_ATTEMPTS;前端不得把短信验证码或 action token 落普通存储。
1.8 实名认证.png
状态:新增。
- 查询:
GET /v1/im/security/realname返回status=unverified|pending|verified|rejected、脱敏姓名/证件号和can_submit。 - 提交:
POST /v1/im/security/realname/submissions,Body{"name":"张三","id_type":"cn_id","id_number":"..."},必须带幂等键。 - 成功:返回
submission_id、status:"pending";前端禁用重复提交,当前以GET /v1/im/security/realname查询状态为准。后续如启用security.realname.updated,应按 0.5.4.6 的info.what="im"业务事件下发。 - 安全:证件号传输和存储加密、展示脱敏;普通日志、搜索索引和消息中禁止出现原值;重复身份由租户策略决定并返回稳定错误。
1.9 实名认证-已实名.png
状态:新增。
- 页面只调用
GET /v1/im/security/realname。verified时返回如masked_name:"张*"、masked_id_number:"110***********1234"、verified_at。 - 前端只读展示,不缓存原始证件信息,不提供直接修改接口。
- 更正信息使用独立申诉
POST /v1/im/security/realname/appeals,Body{"reason":"...","attachments":[]}(reason必填、≤500 字;attachments最多 9 个 file_id),Bearer 即可,当前无需二次身份验证;不复用本页保存按钮。
1.10 实名认证(安卓).png
状态:新增。
- 登录后
GET /v1/im/bootstrap返回security.realname{status,required,allow_skip,remind_after};只有required=true且未认证时显示弹窗。 - 点击“去认证”进入 1.8;点击稍后仅在
allow_skip=true时本地关闭,并按remind_after控制再次提示。 - 服务端对必须实名的受限写操作仍返回
REALNAME_REQUIRED,前端弹窗不能作为授权依据。
1.11 设置独立密码(安卓).png
状态:新增。
- 设置:
POST /v1/im/settings/independent-password,Body{"password":"1234","purpose":"youth_mode","action_token":"..."};服务端只存强哈希,不返回密码。action_token为一次性 token,会在设置密码时消费;设置成功后响应会返回新的enable_action_token,用于下一步开启青少年模式,前端不要复用设置密码时提交的 token。 - 验证:
POST /v1/im/settings/independent-password/verify返回短时action_token,用于关闭青少年模式或修改密码。 - 青少年模式完整开启和关闭流程见
docs/IM-青少年模式前后端对接文档.md。 - 规则:四位数字、禁止明显连续/重复组合;连续错误返回
TOO_MANY_ATTEMPTS和retry_after_seconds。前端输入框禁截屏/自动填充,离开页面立即清空。 - 忘记密码:跳转 1.7 的短信身份验证,验证后重置,不提供明文找回。
1.12 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
1.12.1 未登录、企业与账号安全
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
POST /v1/im/tenants/resolve |
Header X-Tenant-Code: acme;Body {"tenant_code":"acme","platform":"ios","app_version":"1.0.0"} |
{"tenant":{"code":"acme","name":"示例企业","tenant_desc":null,"branding":{}},"endpoints":{"websocket":"wss://api.118im.com/v0/channels?apikey=AQ...","api":"https://api.118im.com/v1/im","file":"https://api.118im.com/v0/file"},"auth_methods":["password","sms"],"password_policy":{"min_length":8,"max_length":64},"app_update":{"type":"none"}} |
POST /v1/im/public/uploads 企业注册/反馈附件上传 |
Multipart purpose=enterprise_license|tenant_entry_feedback、file=@license.png |
{"file_id":"file_01K...","purpose":"enterprise_license","mime_type":"image/png","size":1024000,"expires_at":"2026-08-25T08:30:15Z"} |
POST /v1/im/tenant-applications 企业注册申请 |
{"credit_code":"91310000MA1K000000","enterprise_email":"admin@example.com","license_file_id":"file_01K...","tenant_code_hint":"acme","client_context":{"platform":"android","app_version":"1.0.0"}} |
{"application_id":"entapp_01K...","status":"received","estimated_review_hours":24,"submitted_at":"2026-08-24T08:30:15.123Z"} |
POST /v1/im/tenant-entry-feedback 企业入口问题反馈 |
{"description":"企业码 acme 提示不可用,无法进入企业。","contact":"user@example.com","tenant_code_hint":"acme","screenshot_file_ids":["file_01K..."],"client_context":{"platform":"ios","app_version":"1.0.0"}} |
{"feedback_id":"entryfb_01K...","status":"received","submitted_at":"2026-08-24T08:35:15.123Z"} |
POST /v1/im/auth/verification-codes 注册发码 |
{"scene":"register","phone":"15500000000"} |
{"challenge_id":"chg_123","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60} |
POST /v1/im/auth/verification-codes/{challenge_id}/verify |
{"code":"123456"} |
{"verification_token":"verify_token_123","expires_in_seconds":300} |
POST /v1/im/auth/verification-codes/{challenge_id}/verify 邀请码 |
{"code":"AbCdE"} |
{"verification_mode":"invite_code","invite_accepted":true} |
POST /v1/im/auth/verification-codes 找回密码发码 |
{"scene":"reset_password","phone":"15500000000"} |
{"challenge_id":"chg_reset_123","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60} |
POST /v1/im/auth/password-reset/challenges/{challenge_id}/verify |
{"code":"123456"} |
{"reset_token":"reset_token_123","expires_in_seconds":300} |
POST /v1/im/auth/password-reset/confirm |
{"reset_token":"reset_token_123","new_password":"NewPass123"} |
{"status":"ok"} |
POST /admin/v1/tenant/users/{user_id}/reset-password 管理后台 |
{"new_password":"AdminReset123","reason":"用户无手机号无法自助找回"} |
{"user_id":"usr...","status":"reset","auth_version":2} |
POST /v1/im/auth/logout |
{} |
{"status":"ok"} |
GET /v1/im/app/releases/latest?platform=ios|android|web¤t_version=1.0.0&channel=stable |
Query only | {"current_version":"1.0.0","has_update":true,"force_update":false,"platform":"web","channel":"stable","latest_version":"1.1.0","min_supported_version":"1.0.0","update_type":"recommended","package_url":"","store_url":"","release_notes":"修复已知问题。","remind_after_seconds":86400} |
2. 消息首页、全局导航与系统通知
2.1 消息.png
状态:会话订阅已实现;产品摘要和系统入口需改造。
- 登录后发送
sub me,再通过get拉取sub元数据;每项使用topic、public、private、updated、seq/read/recv计算名称、摘要和未读数。 - 消息增量通过各 Topic 的
{data}更新;未读数为max(seq-read,0),回执以服务端游标为准,不能只用本地计数。 GET /v1/im/bootstrap返回固定入口、功能开关、权限、系统 Topic、entry_badges和entries;搜索调用GET /v1/im/search/conversations?q=&cursor=。- 本地缓存按
tenant_code + user_id隔离。切租户、退出或账号变化时不得复用另一身份的会话摘要。
2.2 消息-无网络状态.png
状态:需改造客户端恢复流程和消息幂等。
- 纯网络状态由客户端监听;断网时展示缓存并禁止需要在线确认的写操作,未发送消息进入本地
pending队列。 - 使用指数退避加随机抖动重连;恢复后严格执行
hi -> token login -> sub me -> 恢复 Topic -> get data。 - 每个 Topic 按最后持久化的
seq/read/recv增量拉取。重发消息复用x-im-client-mid,服务端需实现去重并返回原seq。 - Token 失效转登录页;租户停用返回企业不可用页;禁止无限快速重连。
2.3 消息-更多.png
状态:菜单为纯客户端;权限数据新增。
- 菜单打开/关闭不调用服务端。菜单项来自登录后
GET /v1/im/bootstrap的features与permissions。 create_group=false时隐藏“创建群聊”;user_search=false时隐藏“添加好友”;扫码权限只影响实际扫码,不影响服务端授权。- 各入口协议分别见 5.3、6.1、10.1;服务端在实际写接口再次鉴权,不能只依赖隐藏按钮。
2.4 消息页面—长按弹出.png
状态:基础消息操作部分已实现;产品权限矩阵需改造。
- 长按本身不请求。客户端根据消息的
type/from/seq/created_at/status、当前 Topic 权限和服务端下发的message_policy预裁剪菜单。 - 引用/复制为本地动作;收藏见 4.16;转发见 4.17~4.20;删除使用 MeetingIM
{del what:"msg",topic,delseq};撤回需新增POST /v1/im/topics/{topic}/messages/{seq}/recall。 - 编辑使用
POST /v1/im/topics/{topic}/messages/{seq}/revisions,Body{"content":...,"client_mid":"..."};仅文本且在时限内允许。 - 群回执调用
GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor=。所有最终操作由服务端验证消息归属、时限、群角色和租户。
2.5 系统消息.png
状态:需改造。
GET /v1/im/system-notifications?cursor=返回id,event_type,title,summary,created_at,read_at,action;实时刷新以 0.5.4.6 的 WS{info what:"im|group"}业务事件为准,系统通知列表记录不等同于每一条实时事件。summary为服务端生成的可直接展示文案(如「对方已同意你的好友申请」「对方已婉拒你的好友申请」),前端直接渲染,无需再做状态码到文案的映射;action读路径统一归一为{type,target_id,payload,...原始键}。- 新设备登录事件内容为
device_name,platform,ip_masked,login_at,security_action;IP 只下发必要脱敏值。 - 已读:
POST /v1/im/system-notifications/{id}/read,带幂等键;重复已读返回首次持久化的read_at,不回显本次提交值。跳转目标只接受服务端白名单action.type和内部参数。 - 当前普通认证用户可向
sysTopic 发布的路径必须封禁。客户端不得渲染普通用户伪造的系统卡片为可信通知。
2.6 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
2.6.1 登录后启动、消息首页与系统通知
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/bootstrap |
Query only | {"features":{"create_group":true,"user_search":true,"scan_qr":true,"system_notifications":true,"message_recall":true,"message_edit":true,"audio_call":true,"video_call":true},"permissions":{"create_group":true,"user_search":true,"scan_qr":true,"audio_call":true,"video_call":true},"entries":{"new_friends":{"enabled":true,"route":"/contacts/new-friends","unread_count":0},"groups":{"enabled":true,"route":"/contacts/groups","unread_count":0},"file_assistant":{"enabled":true,"route":"/chat/file-assistant","topic":"slf","unread_count":0},"system_messages":{"enabled":true,"route":"/chat/system","topic":"sys","unread_count":0}},"entry_badges":{"new_friends_pending_count":0,"groups_unread_count":0,"file_assistant_unread_count":0,"system_unread_count":0,"workbench_total_unread":0},"system_topic":{"topic":"sys","type":"system"},"message_policy":{"recall_window_seconds":120,"edit_window_seconds":300,"supported_actions":["copy","quote","delete","recall","edit"]},"security":{"realname":{"status":"unverified","required":false,"allow_skip":true,"remind_after":"24h"}}} |
GET /v1/im/search/conversations?q=&cursor= |
Query only | {"items":[{"topic":"grp...","public":{"name":"项目群"},"private":{"pinned":false,"mute_notifications":false},"seq":12,"read":10,"recv":12,"unread":2,"updated_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
GET /v1/im/system-notifications?cursor=&limit=20 |
Query only | {"items":[{"id":"evt_1","event_type":"friend.request.created","title":"新的好友申请","summary":"张三请求添加你为好友","action":{"type":"friend_request","target_id":"usr...","payload":{"request_id":"frq_1"},"request_id":"frq_1"},"read_at":null,"created_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
POST /v1/im/system-notifications/{id}/read |
{"read_at":"2026-08-20T09:00:00Z"} |
{"status":"ok","id":"evt_1","read_at":"2026-08-20T09:00:00Z"} |
3. 单聊会话、会话建立与消息状态
3.1 个人对话框(对方的状态).png
状态:需改造好友关系状态。
- 进入会话先
GET /v1/im/conversations/{topic}/context,返回relationship=pending_inbound、can_send=false、对方公开资料和申请 ID。 - 历史申请提示由
relationship_event系统消息渲染;普通消息仍通过 Topic{data}下行。 - 输入区锁定;“通过/拒绝”调用 5.2 的好友申请接口。前端不可通过直接
pub绕过,服务端发布路径必须校验好友关系。
3.2 个人对话框(对方的状态)(1).png
状态:在线状态已具备基础能力;关系文案新增。
- 订阅 P2P Topic 后用
{pres}更新在线/离线,last_seen仅在对方隐私允许时展示。 GET /v1/im/conversations/{topic}/context返回display_name,avatar_file_id,online_visibility,relationship。- 好友关系变化由服务端生成 0.5.4.6 定义的 WS 业务事件,客户端不得根据本地动作自行永久插入系统通知记录。
3.3 个人对话框(我的状态).png
状态:新增好友申请状态。
- 发起申请使用
POST /v1/im/friend-requests,Body{"target_user_id":"usr...","message":"我是...","source":"search"}。 - 返回
request_id,status:"pending",created_at;页面以该服务端状态显示“已发送”,重复点击用同一幂等键返回原申请。 - 申请待处理时
context.relationship=pending_outbound、can_send=false,禁止重复创建和普通消息发送。
3.4 个人对话框(我的状态)(1).png
状态:发送和读回执已实现基础能力;送达状态映射需统一。
- 本地生成
client_mid后显示sending;pub收到202 + seq更新为sent;失败保留同一client_mid重试。 - 对方
note what:"recv"更新delivered,note what:"read"更新read;多设备取服务端最大游标。 - 状态只附着在气泡元数据,不修改消息正文。服务端拒绝时映射为
failed(reason),不得伪装成已发送。
3.5 开启沟通页面.png
状态:新增好友审批与 Topic 联动。
- 好友申请通过后,服务端在同一事务内建立双方关系和 P2P 订阅,并向申请人、通过人本人下发
info.what="im", event="friend.request.accepted";申请人当前会写入系统通知,通过人本人当前只收到实时业务事件。 - 客户端收到事件或重新查询
context.relationship=friend后解锁输入区;不得仅因本地“通过”按钮成功动画解锁。 - 首次进入订阅 P2P Topic,再按
since拉取申请上下文和正常消息。
3.6 双方沟通状态.png
状态:上传带 topic 与下载按会话授权已在服务端实现;客户端按本节接入。
- 选择媒体后本地生成
client_mid和稳定占位;调用文件上传(必须带topic=<该会话 topic>,服务端校验成员资格并自动建立文件-会话引用),得到file_id后再pub x-im-type=image|video|voice。 content至少含file_id,name,size,mime,width,height,duration_ms,thumbnail_file_id中适用字段;上传进度是客户端任务状态,不写成聊天消息。- 服务端在上传阶段即建立文件与会话的引用(带
topic);消息发布成功后按消息附件引用建立消息级 ACL;文件下载校验会话成员身份与消息可见性。 - 媒体文件(缩略图、原图、视频、语音)一律走同一次上传流程并带同一个
topic;缩略图也必须挂到会话,否则对端只能看到占位图。 - 上传失败(网络中断、超时、5xx)按整文件重传处理:重新调用
POST /v0/file/u/上传完整文件,不做分片/断点续传;每次重传得到新file_id,旧file_id直接放弃、由服务端 TTL 回收,客户端不得再引用。 - 上传成功但
pub失败时不重新上传,复用已上传file_id和原client_mid重发消息,服务端按client_mid幂等去重。
3.7 个人对话—拉黑状态/已读状态.png
状态:黑名单产品规则新增。
- 拉黑后
context.relationship=blocked_by_me|blocked_by_peer、can_send=false;服务端pub返回统一MESSAGE_NOT_ALLOWED,不得向发送者泄露具体是对方拉黑。 - 失败消息只保存在发送端本地并显示失败,不下发给对方、不写入正常消息表。
- 已读仍按历史最大
read渲染;拉黑不能回滚既有回执。解除拉黑见 5.10、5.12。
3.8 个人对话—删除状态.png
状态:新增好友删除后的发送约束。
- 删除好友使用
DELETE /v1/im/friends/{user_id};成功后双方关系变为none,服务端推送关系变化事件。 - 后续
pub返回FRIEND_RELATION_REQUIRED;前端保留失败草稿,提供“重新添加好友”入口,不自动发申请。 - 历史聊天是否保留由 Topic 权限决定,删除好友不等于删除双方历史消息。
3.9 个人对话-键盘收起状态.png
状态:纯客户端。
- 键盘、表情和附加面板收起不调用服务端;保持列表锚点和未发送草稿。
- 草稿默认按
tenant + user + topic本地加密保存;若实现多端草稿,再单独调用PUT /v1/im/topics/{topic}/draft,不得混入聊天消息。
3.10 个人对话-发各种文件状态.png
状态:面板纯客户端;上传需改造。
- 打开面板不请求。选择前依据
context.capabilities判断图片、视频、语音、文件是否允许,调用系统选择器/相机。 - 上传前校验
mime,size和bootstrap.limits.max_file_size;服务端仍必须重新校验租户配额、文件类型和会话权限。 - 具体上传/发消息使用 3.6 协议;相机和麦克风授权见 10.20、10.21。
3.11 个人对话-发各种文件状态(1).png
状态:需改造产品消息 Schema。
- 客户端按
x-im-type选择文本、名片、文件等卡片,公共字段统一使用from,topic,seq,ts,head,content。 - 每种卡片必须实现未知字段忽略、未知类型占位、发送/失败/回执状态和长按动作。
- 重新安装或换设备后仅凭服务端消息即可还原卡片,不能依赖发送端本地路径或临时 URL。
3.12 个人对话-发送名片状态.png
状态:新增产品消息类型。
- 选人:
GET /v1/im/friends?query=&cursor=;选择后发送x-im-type:"contact_card"。 content为{"user_id":"usr...","display_name":"张三","avatar_file_id":"...","public_account":"A1024"};服务端按目标用户当前公开资料生成/校正快照,禁止手机号、证件号等敏感字段。- 点击卡片调用
GET /v1/im/users/{identifier}/profile;identifier可传user_id、username或public_account,用户不可见或已离职时显示失效卡片,不跨租户查询。
4. 消息输入、媒体内容与消息操作
4.1 键盘展开.png
状态:纯客户端。
- 键盘展开、列表上推和光标变化不调用服务端;草稿按
tenant + user + topic保存。 - 点击发送时才校验
context.can_send并执行pub text;正文去首尾策略、最大长度从bootstrap.limits.max_text_length获取,服务端再次校验。
4.2 @人员—选择提醒人.png
状态:新增群成员查询。
- 输入
@后调用GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true,返回user_id,display_name,avatar_file_id,role,mentionable。 @所有人是虚拟项,只有响应permissions.can_mention_all=true时展示,不以特殊用户 ID 表示。- 搜索做 300ms 防抖并取消旧请求;服务端只返回当前租户且仍在群内的成员。
4.3 @人员—完成.png
状态:纯客户端选择;发送协议需改造。
- 完成后在编辑器插入带范围的 Mention 节点,内部保存
user_id,显示昵称只是快照。 - 发送时
pub.head.mentions=[{"user_id":"usr...","offset":0,"length":3}];@所有人使用mention_all:true,不得伪造用户 ID。 - 服务端验证成员身份和
mention_all权限;无效成员可删除提醒语义但保留可读文本,越权@所有人返回MENTION_ALL_FORBIDDEN。
4.4 @人员状态.png
状态:需改造提醒和 Push。
- 消息仍通过
{data}下行,客户端使用结构化mentions高亮,禁止通过字符串扫描昵称判断。 - 当前用户被提及时会话摘要标记
mentioned=true;读到对应seq后随note read清除。 - Push 服务按结构化目标生成“有人@你”文案;已退群用户不能收到新提醒,但历史消息仍按快照渲染。
4.5 表情包展开(发送按钮默认状态).png
状态:纯客户端。
- 面板、分类、最近使用和删除键均本地处理;未选择内容且文本为空时禁用发送。
- Unicode 表情作为普通
text内容发送。自定义表情资源首版未定义,不得把本地资源路径直接发送。
4.6 表情包发送页面.png
状态:Unicode 表情使用已实现文本消息;独立表情包新增。
- 文本表情按
pub x-im-type:text发送。独立自定义表情若启用,使用x-im-type:"sticker",content={pack_id,sticker_id,file_id,width,height}。 - 自定义表情发送前服务端校验表情包属于当前租户且可用;未知表情显示占位,不使用任意外链 URL。
4.7 语音—按住说话.png
状态:录音纯客户端;上传和语音消息需改造。
- 按下前检查麦克风权限和
context.capabilities.voice;权限不足进入 10.21,不发请求。 - 松开发送:编码为服务端允许的 MIME,校验最短/最长时长,上传后
pub x-im-type:voice,内容含file_id,duration_ms,size,mime,waveform。 - 上滑取消、过短、系统来电打断时删除临时文件,不调用上传接口。
4.8 语音按住说话.png
状态:纯客户端。
- 音量幅度、上滑取消区域和浮层颜色来自本地录音会话,不上传实时音量。
- 进入取消区只改变本地状态;最终抬手决定删除或执行 4.7 的上传发送。
4.9 内容沟通对话3-语音录音中.png
状态:录音纯客户端;临时状态可选改造。
- 录音期间客户端锁定其他媒体入口。默认不向群成员广播“正在录音”,以减少隐私暴露和状态风暴。
- 若产品确认需要,可发送不落库的
{note what:"kpa",topic};服务端限频,其他端超时自动消失,不能作为聊天记录。
4.10 内容沟通对话3-图片和语音发送中.png
状态:客户端任务管理新增;服务端上传需改造。
- 每个待发送项保存
client_mid,local_uri,type,upload_id,progress,state;upload_id仅在单次上传任务内有效(进度上报与本地去重用)。 - 重试即新建上传任务:
progress归零、生成新upload_id,整文件重传POST /v0/file/u/,不做分片/断点续传;重传成功后消息体file_id等字段全部取最新一次上传响应。 - 自动重试采用指数退避且不超过 3 次,之后转人工点按重试;App 冷启动扫描本地
failed任务队列重新上传。 - 上传完成后再发布消息;多任务可并发但必须各自保持原排序号。离开页面不取消,退出账号必须暂停并隔离任务。
- 服务端返回
FILE_TOO_LARGE、FILE_TYPE_NOT_ALLOWED、STORAGE_QUOTA_EXCEEDED时不可自动无限重试。
4.11 内容沟通对话3-语音/视频/图片状态.png
状态:需改造媒体 Schema 和受权下载。
- 语音渲染
duration_ms,waveform,未听状态保存在客户端并可选跨端同步;视频渲染thumbnail_file_id,duration_ms,width,height;图片渲染缩略图和原图 ID。 - 预览前使用认证文件接口获取,服务端校验当前用户仍可访问引用消息;附件撤回或过期返回
FILE_UNAVAILABLE。 - 缩略图和原文件分别授权,客户端缓存键必须包含租户与用户。
4.12 语音图片发送状态.png
状态:需改造。
- 图片选择后立即显示本地缩略图,上传成功后只替换资源来源,不改变
client_mid或列表位置。 - 失败展示重试按钮;重试复用同一上传任务和消息幂等 ID。语音播放与图片上传状态相互独立。
4.13 上传文件.png
状态:纯客户端视觉资源。
- 该图片仅定义统一上传图标和无障碍名称“上传文件”,不产生网络请求。
- 文件助手、群文件、会话附件、反馈证据实际上传时统一走 0.2 文件接口,并额外提交业务
purpose以便服务端执行权限和保留期策略。
4.14 文件发送状态.png
状态:上传已实现基础能力;消息引用和 ACL 需改造。
- 上传成功后发送
x-im-type:file,content={file_id,name,size,mime,extension,checksum};文件名做长度和控制字符清理。 - 服务端在消息落库事务中关联文件;未关联的临时上传按 TTL 清理。卡片下载见 7.9~7.11。
- 多文件逐个生成
client_mid,某个失败不能改变其他文件状态。
4.15 多选-选中对话框.png
状态:选择纯客户端;批量动作新增。
- 客户端按
(topic,seq)保存已选项并显示数量;默认最多 100 条,超限在本地阻止。 - 转发、收藏、删除前把选中列表提交对应接口;服务端逐条校验仍可见、类型支持和权限,返回
succeeded[]与failed[{seq,reason}]。 - 不允许只因第一条可操作就假定整批可操作。
4.16 多选-选中对话内容-收藏成功.png
状态:新增。
POST /v1/im/favorites/batch,Body{"messages":[{"topic":"grp...","seq":"123"}]},带幂等键。- 返回
created_count,existing_count,failed[];同一用户对同一topic+seq重复收藏不新增记录。 - 收藏保存原消息引用、不可变展示快照和附件授权关系;成功 Toast 只在至少一项成功时展示,部分失败需给出汇总。
4.17 转发-单选.png
状态:新增。
- 目标列表:
GET /v1/im/forward-targets?query=&cursor=,只返回can_receive_forward=true的会话和文件助手。 - 选中后本地进入确认页,不立即发送;服务端在最终转发接口再次校验目标 Topic、原消息和附件权限。P2P 目标可以传转发目标列表返回的
topic,也可以传对方usr...用户 ID,服务端会归一化为当前用户与对方的 P2P topic。
4.18 转发-多选.png
状态:新增。
- 目标查询同 4.17;多选最多取
bootstrap.limits.max_forward_targets,首版建议 9。 - 确认前只保存目标 Topic 字符串或对方
usr...用户 ID;不得缓存或提交tenant_id。跨租户 Topic 在服务端统一拒绝。
4.19 转发—逐条转发.png
状态:新增。
POST /v1/im/messages/forward,Body{"mode":"separate","sources":[{"topic":"p2p...","seq":12}],"target_topics":["grp..."],"comment":"..."}。seq兼容数字和字符串;P2P 源会话也可以传对方usr...用户 ID,服务端会归一化为 P2P topic。- 返回每个目标和源消息的
client_mid/seq/status;status取值为sent、pending、failed。pending表示服务端已分配seq/client_mid,但 Kafka 投递结果暂不确定,客户端应按“发送中”展示,保留同一个Idempotency-Key重试或等待后续消息同步,不要立即生成第二条本地消息。服务端保持源顺序,重新生成目标消息,不能复用原seq。 - 附件必须建立新的授权引用;原消息已撤回、过期或不可见时该项失败。留言作为每个目标的一条独立文本消息发送,返回项可带
comment_client_mid/comment_seq/comment_status/comment_reason;转发消息本身仍保持原head/content,不得把留言包装进图片、文件等原消息content。
4.20 转发—合并转发—多选状态.png
状态:新增。
- 调用 4.19 接口,
mode:"bundle";服务端生成一条forward_bundle目标消息,内容含bundle_id,title,item_count,preview[],items[],source_refs[]。首版 Bundle 明细以内联快照随消息下发,后续如消息量增大再扩展GET /v1/im/forward-bundles/{bundle_id}?cursor=分页详情。 items[]和preview[]保留from原发送人 ID,并新增from_user用户摘要:{"user_id":"usr...","display_name":"张三","avatar_file_id":"file_avatar"}。头像为空时可缺省;若资料查询失败,服务端至少返回user_id/display_name兜底。- 快照保留显示名、时间和内容摘要,对撤回/敏感字段按策略脱敏;不得允许接收端利用 Bundle 获取原会话额外权限。
- 多目标共享不可变 Bundle 内容但分别生成目标消息与授权记录。
4.21 会话与消息接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
4.21.1 会话上下文、消息操作与检索
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/conversations/{topic}/context |
Path p2p... 或 grp... |
{"topic":"p2p...","type":"p2p","relationship":"friend","can_send":true,"capabilities":{"text":true,"image":true,"file":true,"voice":true,"can_call":true},"peer":{"user_id":"usr...","display_name":"张三","avatar_file_id":""}} |
POST /v1/im/conversations/p2p |
{"peer_user_id":"usr..."} |
{"topic":"p2p...","relationship":"friend","can_send":true} |
GET /v1/im/conversations/{topic}/settings |
Path topic | {"pinned":false,"mute_notifications":false,"message_retention":"default","blocked":false,"remark":"","version":1,"permissions":{"can_update":true}} |
PATCH /v1/im/conversations/{topic}/settings |
{"pinned":true,"mute_notifications":false,"version":1} |
{"pinned":true,"mute_notifications":false,"message_retention":"default","blocked":false,"remark":"","version":2} |
GET /v1/im/conversations/{topic}/messages |
服务端当前为空占位:GET 恒返回空列表,勿用 ?since=&limit= 调它;历史消息(首屏与向上翻页)一律走 WS get data 的 since/before/limit(向上翻页见 0.5.1 约定) |
{"topic":"p2p...","items":[],"next_cursor":"","has_more":false} |
PUT /v1/im/topics/{topic}/draft 待实现 |
{"content":"未发送草稿","version":1} |
{"topic":"grp...","content":"未发送草稿","version":2,"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/topics/{topic}/messages/{seq}/recall |
单条撤回;topic 可传 p2p... 或 grp...;Body {},可选 {"version":1,"reason":"mistake"} |
{"topic":"grp...","seq":12,"status":"recalled","version":2,"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/topics/{topic}/messages/recall |
多选批量撤回;{"items":[{"seq":12,"version":1},{"seq":13}],"reason":"mistake"};items 1~50 条,version 可选 |
{"topic":"grp...","items":[{"topic":"grp...","seq":12,"status":"recalled","version":2,"updated_at":"2026-08-20T09:00:00Z"},{"seq":13,"status":"failed","http":409,"code":30006,"reason":"VERSION_CONFLICT","message":"message version conflict"}]} |
GET /v1/im/topics/{topic}/message-mutations?since_version=&limit= |
拉取撤回/编辑等原地消息变更;since_version 取本地已应用的会话级 mutation_version |
{"topic":"grp...","items":[{"seq":12,"version":2,"event":"message.recalled","operator":"usr...","mutated_at":"2026-08-20T09:00:00Z","mutation_version":3}],"next_cursor":"3","has_more":false,"mutation_version":3,"expired":false} |
POST /v1/im/topics/{topic}/messages/{seq}/revisions |
{"content":{"text":"修改后的文本"},"client_mid":"mid_1","version":1} |
{"topic":"grp...","seq":12,"status":"edited","version":2,"updated_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor= |
Query only | {"topic":"grp...","seq":12,"items":[{"user_id":"usr...","read_at":"2026-08-20T09:00:00Z","recv_at":"2026-08-20T08:59:00Z"}],"next_cursor":""} |
GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor= |
Query only | {"topic":"grp...","items":[{"seq":12,"from":"usr...","ts":"2026-08-20T09:00:00Z","snippet":"命中的文本","matches":[{"start":0,"end":2}]}],"next_cursor":""} |
GET /v1/im/topics/{topic}/messages/search?type=file&q=&cursor= |
Query only | {"topic":"grp...","items":[{"seq":12,"from":"usr...","ts":"2026-08-20T09:00:00Z","file":{"file_id":"file_1","name":"方案.pdf","size":1024,"mime":"application/pdf"},"availability":"available"}],"next_cursor":""} |
GET /v1/im/topics/{topic}/messages/{seq}/context?before=20&after=20 |
Query only | {"topic":"grp...","anchor_seq":12,"items":[{"seq":11,"content":{"text":"上一条"}},{"seq":12,"content":{"text":"命中消息"}}]} |
GET /v1/im/topics/{topic}/media?type=image&cursor= |
Query only | {"topic":"grp...","items":[{"seq":12,"thumbnail_file_id":"file_thumb","file_id":"file_img","width":1080,"height":720,"ts":"2026-08-20T09:00:00Z","availability":"available"}],"next_cursor":""} |
消息撤回和编辑成功后,Topic 在线成员会收到 0.5.4.6 定义的 info.what="im" 业务事件:message.recalled 或 message.edited。批量撤回按成功项逐条产生 message.recalled 事件;失败项不会产生事件。前端收到后按 topic + seq + version 刷新本地消息,不要等待用户重新进入会话才更新。
4.21.2 群消息撤回与批量撤回
群聊撤回使用 Topic 消息操作接口,topic 必须传真实群 Topic(grp...),不能传用户 ID、群 ID 昵称或本地会话别名。单条撤回和多选批量撤回都要求登录态,Header 至少包含 Authorization: Bearer <token>、X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID。
单条撤回:
POST /v1/im/topics/{topic}/messages/{seq}/recall
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
topic |
String | 是 | 群 Topic,例如 grp6PIoZgdpc2g。 |
seq |
Int64 | 是 | 要撤回的消息序号。 |
Body:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
version |
Int | 否 | 期望消息版本。不传时后端使用当前消息版本;传旧版本可能返回版本冲突。 |
reason |
String | 否 | 撤回原因,当前主要用于客户端/审计扩展,可传空。 |
单条请求示例:
{
"version": 1,
"reason": "mistake"
}
单条成功响应 data:
{
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"status": "recalled",
"version": 2,
"updated_at": "2026-09-15T04:30:00Z",
"recalled_at": "2026-09-15T04:30:00Z",
"recalled_by": "usr_actor"
}
多选批量撤回:
POST /v1/im/topics/{topic}/messages/recall
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
topic |
String | 是 | 群 Topic,例如 grp6PIoZgdpc2g。 |
Body:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
items |
Array | 是 | 要撤回的消息列表,长度 1~50。 |
items[].seq |
Int64 | 是 | 要撤回的消息序号。 |
items[].version |
Int | 否 | 该消息期望版本。不传时后端使用当前消息版本。 |
items[].reason |
String | 否 | 单条撤回原因;可不传。 |
reason |
String | 否 | 批量默认撤回原因;单条 items[].reason 可覆盖。 |
批量请求示例:
{
"items": [
{
"seq": 102,
"version": 1
},
{
"seq": 103,
"version": 1
}
],
"reason": "mistake"
}
批量响应始终按条目返回处理结果。一条失败不影响其他条目;成功条目会包含最新 version,失败条目会包含该条失败原因。
{
"topic": "grp6PIoZgdpc2g",
"items": [
{
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"status": "recalled",
"version": 2,
"updated_at": "2026-09-15T04:30:00Z",
"recalled_at": "2026-09-15T04:30:00Z",
"recalled_by": "usr_actor"
},
{
"seq": 103,
"status": "failed",
"http": 409,
"code": 30006,
"reason": "VERSION_CONFLICT",
"message": "message version conflict"
}
]
}
常见失败项:
http |
code |
reason |
说明 |
|---|---|---|---|
400 |
30000 |
MALFORMED_REQUEST |
seq 非法、items 为空或超过 50 条。 |
403 |
30005 |
PERMISSION_DENIED |
当前用户无权撤回该消息,或消息已超过撤回窗口。超过窗口时 message 为 message recall window expired。 |
404 |
30004 |
MALFORMED_REQUEST |
消息不存在。 |
409 |
30006 |
VERSION_CONFLICT |
请求版本与服务端当前消息版本不一致,或消息已撤回/删除。 |
权限与时限:
- 发送人可撤回自己的消息。
- 群主/管理员可撤回群成员消息。
- 当前后端撤回窗口为 120 秒;超过后返回
message recall window expired。 - 被撤回消息服务端会清空普通正文,历史拉取应展示撤回占位,不继续展示旧内容。
实时事件:
撤回成功后,Topic 在线成员会收到 message.recalled。批量撤回按成功项逐条发送事件,失败项不会发送。
{
"info": {
"topic": "grp6PIoZgdpc2g",
"from": "usr_actor",
"what": "im",
"event": "message.recalled",
"payload": {
"topic": "grp6PIoZgdpc2g",
"seq": 102,
"version": 2
}
}
}
客户端处理规则:
- 多选撤回优先调用批量接口,不要并发打很多单条接口。
- 批量接口 HTTP
200只表示请求被服务端处理,仍需逐条读取items[].status。 status="recalled"时,用topic + seq + version更新本地消息为撤回状态。status="failed"时,只对该条提示失败原因,其他成功项正常生效。- 收到实时事件后也按
topic + seq + version刷新本地消息,避免只依赖发起方本地状态。
4.21.3 消息变更同步水位
撤回、编辑属于 seq 不变的原地变更。在线且已订阅 Topic 时,客户端会收到 message.recalled / message.edited 实时事件;单聊中如果客户端在线但当前没有订阅该 P2P,服务端会额外向双方用户通道下发 message.mutation 脏标记事件;离线或错过实时事件时,需要靠会话级水位补偿。
单聊未订阅脏标记示例:
{
"info": {
"topic": "usr_target",
"from": "usr_actor",
"what": "im",
"event": "message.mutation",
"payload": {
"topic": "p2pJaOWxMvALjB4i1K_h_NZgQ",
"seq": 12,
"version": 2,
"event": "message.recalled"
}
}
}
收到 message.mutation 后不要直接改本地消息内容,先把 payload.topic 标记为脏,再调用:
GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}&limit=50
会话列表同步时,me.meta.sub[].private 会带:
{
"mutation_version": 3
}
客户端按账号 + topic 保存本地已应用水位。若服务端 mutation_version 大于本地值,调用:
GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}&limit=50
响应 data:
{
"topic": "grp6PIoZgdpc2g",
"items": [
{
"seq": 102,
"version": 2,
"event": "message.recalled",
"operator": "usr_actor",
"mutated_at": "2026-09-15T04:30:00Z",
"mutation_version": 3
}
],
"next_cursor": "3",
"has_more": false,
"mutation_version": 3,
"expired": false
}
字段说明:
| 字段 | 说明 |
|---|---|
seq |
被变更消息的序号。 |
version |
变更后的消息版本,用于同一 seq 的幂等与乱序保护。 |
event |
message.recalled / message.edited,与实时 info.event 保持一致。 |
operator |
操作人用户 ID。 |
mutated_at |
变更发生时间。 |
mutation_version |
会话级最新水位。客户端成功应用返回项后推进本地水位。 |
next_cursor |
当前实现等同最新 mutation_version,客户端原样保存即可。 |
expired |
当前轻量实现固定为 false;后续若服务端裁剪变更记录,该字段会用于提示客户端降级重拉。 |
客户端处理规则:
mutation_version是会话级水位,不是单条消息的version。- 本地只做
服务端 mutation_version > 本地 mutation_version比较,不要假设连续递增。 - 补偿接口返回的是最新
limit条(默认 50、上限 100)变更的状态快照,非严格增量;变更条数超过limit时更早的变更不再返回。撤回窗口 120 秒、编辑窗口 300 秒,超窗变更无需客户端处理。 - 拉到
message.recalled后,将本地topic + seq标记为撤回,并只在version更高时覆盖。 - 拉到
message.edited后,按seq + version拉取或覆盖消息内容。 - 实时事件和补偿接口可能重复到达,客户端必须按
topic + seq + version幂等处理。
5. 通讯录、好友关系与黑名单
5.1 通讯录.png
状态:新增产品通讯录接口。
- 好友列表:
GET /v1/im/friends?query=&initial=&cursor=,返回user_id,display_name,remark,avatar_file_id,initial,online,online_visibility;好友展示名统一读取display_name,服务端按当前登录人计算,优先级为备注名、昵称、账号。remark仅用于备注编辑页回显,nickname表示对方自己设置的昵称,不应直接作为好友列表或单聊标题的最终展示名。 - 分页:
next_cursor当前按最后一条好友的peer_id生成,客户端必须作为不透明字符串原样回传,不要自行解析、排序或拼接。 - 组织成员:
GET /v1/im/organization/members?query=&department_id=&cursor=;好友和组织成员是两种关系,前端不得混为一个可发消息权限。 - 新朋友、群列表、文件助手和系统消息入口由
GET /v1/im/bootstrap的entries控制显示,初始红点由entry_badges和各入口unread_count返回。新朋友待处理数使用业务统计;文件助手当前返回topic:"slf", unread_source:"topic";系统消息当前返回topic:"sys", unread_source:"business",未读来自系统通知业务表。列表缓存以租户和用户隔离。
5.2 新朋友.png
状态:新增。
- 列表:
GET /v1/im/friend-requests?direction=inbound&status=all&cursor=,返回request_id,applicant,message,source,status,created_at,handled_at。 - 同意:
POST /v1/im/friend-requests/{request_id}/accept;拒绝:POST /v1/im/friend-requests/{request_id}/reject,均带幂等键和可选version。 - 成功返回最终
status和 P2Ptopic。重复审批返回原结果;并发冲突返回REQUEST_ALREADY_HANDLED并附当前状态,前端刷新该项。 - 申请不存在时
accept/reject统一返回404/20004 RESOURCE_NOT_FOUND;通用语义见 0.4。 - 主动下行:创建申请时被申请人收到
info.what="im", event="friend.request.created";通过/拒绝后申请人与处理人本人都收到friend.request.accepted/rejected。前端收到后刷新新朋友列表、好友列表、会话 context 和bootstrap.entry_badges。当前系统通知列表只给被申请人/申请人写记录,处理人本人主要靠实时业务事件刷新。
5.3 添加好友.png
状态:新增。
- 账号搜索:
GET /v1/im/users/search?q=&type=account&cursor=;扫码后调用POST /v1/im/qrcodes/resolve,Body{"token":"...","purpose":"add_friend"}。 - 结果统一返回
relationship=none|friend|pending_inbound|pending_outbound和can_add;隐私不允许或未找到统一为USER_NOT_DISCOVERABLE。 - 分享/二维码见 5.4;发申请见 5.6。服务端按租户配置
allow_user_search和用户隐私再次拦截。
5.4 添加好友(1).png
状态:新增。
- 获取二维码:
POST /v1/im/me/qrcodes,Body{"purpose":"add_friend","expires_in_seconds":86400},返回qr_token,qr_payload,expires_at,display_profile。 - 二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或登录 Token。
- 分享/保存图片是客户端动作;扫码解析始终调用 5.3,过期返回
QR_CODE_EXPIRED,篡改返回QR_CODE_INVALID。
5.5 添加好友-输入精准用户ID.png
状态:新增。
- 精准查询仍调用
GET /v1/im/users/search?q=&type=exact_account;手机号查询仅当租户和目标隐私同时允许时使用type=phone。 - 每项返回公开资料和
action=add|chat|pending|none。前端不得根据是否返回手机号推断账户存在。 - 搜索限流返回
SEARCH_RATE_LIMITED;禁止客户端批量枚举 ID。
5.6 添加好友验证.png
状态:新增。
POST /v1/im/friend-requests,Body{"target_user_id":"usr...","message":"验证留言","source":"search|qr|group","source_topic":"grp..."}。- 留言最多 50 个 Unicode 字符,服务端做敏感内容审核;
source_topic只能是双方当前可见的同租户群。 - 成功返回
request_id,status:"pending";重复有效申请返回原记录,目标隐私关闭时返回统一FRIEND_REQUEST_NOT_ALLOWED。
5.7 好友个人主页.png
状态:新增。
GET /v1/im/users/{identifier}/profile返回按当前查看人裁剪后的username,display_name,remark,avatar_file_id,gender,region,public_account,signature,relationship,capabilities;identifier可传user_id、username或public_account。username是登录账号,nickname是原始昵称,display_name是当前查看人应展示的名称(好友备注优先);三者不得混用。- 查看该用户二维码调用
POST /v1/im/users/{identifier}/qrcodes,Body{"purpose":"add_friend","expires_in_seconds":86400};前端在个人信息页只有用户名时直接把用户名作为identifier传入即可。服务端解析到真实用户后生成二维码,并按资料可见性与add_me_methods.qr做权限校验。 - 发消息使用返回的 P2P
topic;没有 Topic 时调用POST /v1/im/conversations/p2p,Body{"peer_user_id":"..."}并由服务端验证好友关系。 - 音视频按钮只按
capabilities.can_call展示,真正呼叫时再次鉴权。隐藏字段必须从响应中省略,不能只由前端遮挡。
5.8 群聊—非好友个人主页.png
状态:新增。
GET /v1/im/groups/{topic}/members/{user_id}/profile返回群内昵称、角色和允许公开的账号资料。{topic}必须是群聊 topic(grp...);单聊、名片或普通用户资料页使用GET /v1/im/users/{identifier}/profile,不要把usr...用户 ID 放到 groups 路径。can_add_friend同时受群allow_member_add_friend、目标隐私和租户策略控制;为 false 时隐藏添加入口。- 从该页申请时 5.6 传
source:"group"和当前 Topic;服务端校验双方仍在群内。
5.9 好友—聊天设置—未拉黑状态.png
状态:部分 Topic 设置已实现;产品设置新增。
- 查询:
GET /v1/im/conversations/{topic}/settings返回pinned,mute_notifications,message_retention,blocked,remark,permissions。 - 更新置顶/免打扰/保存方式:
PATCH /v1/im/conversations/{topic}/settings,Body 只提交变更字段并带version;返回最新完整设置。 - 备注:
PATCH /v1/im/friends/{user_id},Body{"remark":"..."};查记录见第 7 章,举报见第 11 章,删除见 5.11。
5.10 好友—聊天设置—拉黑状态.png
状态:新增。
- 开启拉黑:
PUT /v1/im/blocks/{user_id};解除:DELETE /v1/im/blocks/{user_id},均带幂等键。 - 成功返回
blocked,relationship,updated_at并向当前用户其他设备推送relationship.blocked;解除拉黑推送relationship.unblocked。事件topic为操作人自己的usr_actor,payload.user_id为操作人 ID;客户端收到后刷新黑名单、好友列表和会话 context。拉黑后服务端拦截对方消息和呼叫。 - 解除拉黑只恢复黑名单 ACL;若双方已非好友,
relationship仍为none,不能自动恢复好友。
5.11 删除好友提示.png
状态:确认框纯客户端;删除接口新增。
- 确认调用
DELETE /v1/im/friends/{user_id},带幂等键。是否清除本地历史由客户端在成功后独立处理,不放入删除好友请求。 - 服务端解除双向好友关系但不替对方删除历史;返回
relationship:"none",topic。当前用户是否删除本地/服务端可见记录是独立选择。 - 取消不请求;确认后清理页面缓存并等待关系事件校正其他设备。
5.12 黑名单.png
状态:新增。
GET /v1/im/blocks?cursor=返回user,blocked_at;移出调用 5.10 的 DELETE。- 批量解除:
POST /v1/im/blocks/batch-delete,Body{"user_ids":[...]},返回逐项结果;最多 100 人。 - 用户资料不可见时仍用受控占位显示黑名单记录,不能因资料删除而无法解除。
5.13 黑名单提示(1).png
状态:确认框纯客户端;写接口新增。
- 确认后调用
PUT /v1/im/blocks/{user_id},相同幂等键重复提交返回当前blocked=true。 - 服务端在同一事务内更新黑名单和通信 ACL,并写审计;取消不调用接口。
- 若对象跨租户或不存在统一返回
RESOURCE_NOT_FOUND。
5.14 黑名单提示.png
状态:图片文件名与画面语义不一致;按“清除聊天记录”实施。服务端已实现并实测(2026-10-09),剩余前端改造。
- 确认调用 MeetingIM
{del what:"msg",topic,delseq:[{"low":1,"hi":last_seq+1}],hard:false},仅删除当前用户视图;或封装DELETE /v1/im/conversations/{topic}/messages。 - 服务端现状(2026-10-09 测试环境实测通过):REST 接口仅写
im_dellog本人区间并递增deleted_id,不改动消息行、对方视角不受影响,本人其他设备经deleted_id同步感知清空;WS{del}语义相同,两条路径二选一即可。对非成员或不存在会话返回 403 FORBIDDEN。 - 不解除好友、不影响对方历史、不删除收藏;附件仅在无任何有效引用且过保留期后回收。
- 前端需在确认文案明确“仅清除自己的聊天记录”,成功后清本地 Topic 消息缓存并保留会话关系。
5.15 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
5.15.1 通讯录、好友、黑名单与二维码
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/friends?query=&initial=&cursor= |
Query only | {"items":[{"user_id":"usr...","display_name":"张三","remark":"","avatar_file_id":"","initial":"Z","topic":"p2p...","relationship":"friend","online_visibility":"unknown","capabilities":{"can_send":true,"can_call":true}}],"next_cursor":""} |
GET /v1/im/friends?query=&cursor= |
Query only | {"items":[{"user_id":"usr...","display_name":"张三","topic":"p2p...","relationship":"friend"}],"next_cursor":""} |
GET /v1/im/friend-requests?direction=inbound&status=all&cursor= |
Query only | {"items":[{"request_id":"frq_1","direction":"inbound","applicant_id":"usr_a","target_user_id":"usr_b","peer":{"user_id":"usr_a","display_name":"张三"},"message":"我是张三","source":"search","status":"pending","created_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
POST /v1/im/friend-requests |
{"target_user_id":"usr...","message":"验证留言","source":"search","source_topic":""} |
{"request_id":"frq_1","applicant_id":"usr_me","target_user_id":"usr...","message":"验证留言","source":"search","status":"pending","topic":"p2p...","created_at":"2026-08-20T09:00:00Z","expires_at":"2026-08-27T09:00:00Z"} |
POST /v1/im/friend-requests/{request_id}/accept |
{"version":1} |
{"request_id":"frq_1","status":"accepted","topic":"p2p...","handled_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/friend-requests/{request_id}/reject |
{"version":1} |
{"request_id":"frq_1","status":"rejected","handled_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/users/search?q=&type=account&cursor= |
Query only | {"items":[{"user_id":"usr...","display_name":"张三","avatar_file_id":"","public_account":"zhangsan","relationship":"none","can_add":true}],"next_cursor":""} |
GET /v1/im/users/search?q=&type=exact_account |
Query only | {"items":[{"user_id":"usr...","display_name":"张三","public_account":"zhangsan","relationship":"none","can_add":true}],"next_cursor":""} |
GET /v1/im/users/{identifier}/profile |
Path user_id/username/public_account |
{"user_id":"usr...","username":"zhangsan7","display_name":"张三","remark":"","avatar_file_id":"","gender":"","region":"","public_account":"zhangsan","signature":"","relationship":"friend","capabilities":{"can_send":true,"can_call":true,"can_add":false},"topic":"p2p..."} |
PATCH /v1/im/friends/{user_id} |
{"remark":"老张"} |
{"user_id":"usr...","remark":"老张","relationship":"friend","updated_at":"2026-08-20T09:00:00Z"} |
DELETE /v1/im/friends/{user_id} |
Path user ID | {"user_id":"usr...","relationship":"none","deleted":true} |
GET /v1/im/blocks?cursor= |
Query only | {"items":[{"user":{"user_id":"usr...","display_name":"张三","avatar_file_id":""},"blocked_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
PUT /v1/im/blocks/{user_id} |
{} |
{"user_id":"usr...","blocked":true,"blocked_at":"2026-08-20T09:00:00Z"} |
DELETE /v1/im/blocks/{user_id} |
Path user ID | {"user_id":"usr...","blocked":false} |
POST /v1/im/blocks/batch-delete |
{"user_ids":["usr_a","usr_b"]} |
{"items":[{"user_id":"usr_a","blocked":false}],"failed":[]} |
POST /v1/im/me/qrcodes |
{"purpose":"add_friend","expires_in_seconds":86400} |
{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=add_friend","expires_at":"2026-08-21T09:00:00Z","display_profile":{"user_id":"usr...","display_name":"张三"}} |
POST /v1/im/users/{identifier}/qrcodes |
{"purpose":"add_friend","expires_in_seconds":86400} |
{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=add_friend","expires_at":"2026-08-21T09:00:00Z","display_profile":{"user_id":"usr...","display_name":"张三","public_account":"zhangsan"}} |
POST /v1/im/qrcodes/resolve |
{"token":"token...","purpose":"add_friend"} |
{"purpose":"add_friend","user":{"user_id":"usr...","display_name":"张三"},"relationship":"none","can_add":true} |
6. 群聊创建、资料维护与成员治理
6.1 群聊.png
状态:MeetingIM 群 Topic 已实现基础能力;产品群列表与配额需改造。
- 列表:
GET /v1/im/groups?category=owned|managed|joined&query=&cursor=,返回topic,name,avatar_file_id,member_count,my_role,updated_at。 - 创建:
POST /v1/im/groups,Body{"name":"...","description":"...","member_user_ids":[],"avatar_file_id":"..."};description可省略或传空字符串,最长 200 字符;原子校验租户群数上限和群成员上限,返回topic,name,avatar_file_id,description,owner,member_count,version,permissions。 - 分类由服务端
my_role决定;创建成功或角色变化后通过info.what="group"业务事件刷新,不由前端自行移动分组。
6.2 空白页.png
状态:纯客户端。
- 对 6.1 任一分类返回
items=[]且has_more=false时显示对应空状态,不追加空白占位数据。 - 收到
group.membership.updated或创建成功后重新拉取当前分类。
6.3 加入群聊.png
状态:新增邀请卡片和加入接口。
- 邀请卡展示以群邀请接口返回和系统通知记录为准;系统通知
event_type=group.invitation.created,action.type=group_invitation_accept,action.invitation_id用于点击加入,action.topic为目标群。相关实时刷新使用info.what="group"群业务事件。 - 点击加入:
POST /v1/im/group-invitations/{invitation_id}/accept,带幂等键;返回status=joined|pending_approval和topic。 - 服务端校验邀请租户、有效期、群状态、成员资格和成员数上限;重复加入返回当前状态,过期返回
INVITATION_EXPIRED。
6.4 内容沟通对话3-开启进群验证与拉人状态.png
状态:新增群成员事件。
- 邀请:
POST /v1/im/groups/{topic}/invitations,Body{"user_ids":[],"message":""};按群策略直接加入或创建待审批申请。 - 加入、审批、拒绝等结果由服务端生成
info.what="group"群业务事件,例如group.member.joined、group.join_request.accepted/rejected。 - 客户端只渲染服务端可信事件;服务端同时更新成员表、Topic 订阅和成员数,不允许三者不一致。
6.5 邀请进群申请.png
状态:新增。
GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=返回申请人、邀请人、来源、时间、状态和version;申请人展示字段直接带display_name/avatar_file_id/user,邀请人展示字段带inviter_display_name/inviter_avatar_file_id/inviter。- 同意:
POST /v1/im/groups/{topic}/join-requests/{id}/accept;拒绝:POST /v1/im/groups/{topic}/join-requests/{id}/reject,带幂等键与version。 - 仅群主/管理员可处理;同意时原子校验群成员上限。并发处理返回
REQUEST_ALREADY_HANDLED和最终状态。
6.6 新邀请进群申请.png
状态:新增。
GET /v1/im/groups/{topic}/summary返回pending_join_request_count、最新公告和权限;新申请通过group.join_request.created推送给有审批权限的成员。- 审批完成后服务端广播新数量;前端归零时移除入口。普通成员响应中不得得到申请人敏感信息或数量。
6.7 群公告状态.png
状态:新增。
- 查询最新公告:
GET /v1/im/groups/{topic}/announcements/latest;发布/更新:PUT /v1/im/groups/{topic}/announcements/current,Body{"content":"...","version":1}。 - 返回
announcement_id,topic,content,author,version,published_at,permissions.can_edit;即使暂无公告也返回空公告和编辑权限。仅有权限角色可写,使用乐观锁防覆盖。 - 发布成功后服务端生成
group.announcement.updated群业务事件;顶部摘要使用接口返回数据,不解析聊天文本猜测。
6.8 群公告状态/邀请进群申请同时存在.png
状态:纯客户端组合;数据接口新增。
- 页面一次调用
GET /v1/im/groups/{topic}/summary,分别读取announcement和pending_join_request_count,按产品固定顺序渲染两个入口。 - 处理申请只刷新计数,不清公告;更新公告只替换公告对象,不影响申请入口。
6.9 群公告(对方查看状态).png
状态:新增。
- 详情:
GET /v1/im/groups/{topic}/announcements/{announcement_id};普通成员响应不含编辑权限。 - 已读:
POST /v1/im/groups/{topic}/announcements/{id}/read,带幂等键,返回read_at;用于清除新公告标记。 - 服务端校验当前群成员资格,退群后不可借历史 ID 读取新公告。
6.10 群聊设置.png
状态:部分订阅设置已实现;产品菜单新增。
GET /v1/im/groups/{topic}/settings返回my_role,permissions,profile,announcement,member_count,pinned,mute_notifications和群治理开关。- 个人置顶/免打扰通过
PATCH /v1/im/groups/{topic}/member-settings;群级设置必须使用 6.17,不混在个人设置里。 - 退出:
POST /v1/im/groups/{topic}/leave;群主必须先转让或解散。清记录、举报分别见 5.14、11.3。 - 群不存在返回
404/30004 RESOURCE_NOT_FOUND;群已解散返回410/30004 GROUP_DISSOLVED;非群成员返回403/30005 PERMISSION_DENIED。通用语义见 0.4。
6.11 群资料.png
状态:需改造。
GET /v1/im/groups/{topic}返回name,avatar_file_id,description,owner,member_count,version,permissions。- 更新:
PATCH /v1/im/groups/{topic},Body 可含name,avatar_file_id,description,version;成功返回最新对象并推送group.profile.updated。 - 服务端做角色、长度、敏感内容、文件归属校验;前端只展示
permissions.can_edit_profile允许的入口。
6.12 设置群头像.png
状态:上传基础已实现;用途绑定需改造。
- 来源选择、裁剪是客户端动作。上传使用
purpose=group_avatar,服务端校验图片 MIME、尺寸、大小并返回file_id。 - 上传后调用 6.11 PATCH
{"avatar_file_id":"...","version":n};只有 PATCH 成功才替换正式头像。 - 取消或 PATCH 失败的临时文件按 TTL 回收,不能污染群文件列表。
6.13 设置昵称(1).png
状态:新增。
PUT /v1/im/groups/{topic}/members/me/profile,Body{"nickname":"...","version":n}。- 服务端校验成员资格、长度和敏感词,返回
nickname,version,updated_at并推送成员资料变化。 - 群不存在返回
404/30004 RESOURCE_NOT_FOUND;群已解散返回410/30004 GROUP_DISSOLVED;非群成员返回403/30005 PERMISSION_DENIED。通用语义见 0.4。 - 该昵称仅作用当前群,不能修改全局用户昵称或其他群资料。
- 群成员列表和群成员资料中的
display_name仍由服务端按当前登录人计算,好友备注优先;群内昵称单独返回为group_nickname,前端不要用nickname/group_nickname覆盖display_name。
6.13.1 群成员列表拉取与缓存策略
状态:已按分页 + 服务端页缓存实现。
客户端进入普通群聊会话时不要主动全量拉取群成员列表;群详情中的 member_count、当前用户权限和会话上下文足够渲染聊天页。只有进入群成员页、管理员候选页、禁言候选页、转让群主候选页或输入 @ 搜索成员时,才调用 GET /v1/im/groups/{topic}/members。
请求参数:
| 参数 | 类型 | 用法 |
|---|---|---|
topic |
Path string | 群 Topic,例如 grpORCbFk4mAzk。 |
query |
Query string,可选 | 成员搜索关键字。空或省略表示拉当前筛选条件下的成员页;有值时服务端按群昵称、用户昵称、备注展示名、用户名等可搜索字段匹配。@ 人、候选人选择器必须优先传 query,不要先拉全量再本地筛。 |
cursor |
Query string,可选 | 分页游标。第一页省略或传空;下一页必须使用上一次响应的 next_cursor,客户端不得自行拼游标。 |
limit |
Query int,可选 | 每页数量,默认 30,最大 100。万人群场景建议普通列表 30,搜索/候选页 30,不要循环预拉完整群成员。 |
mentionable |
Query bool,可选 | @ 成员选择器传 true,服务端只返回当前可被提及的成员,并在响应里返回 permissions.can_mention_all;普通成员列表省略或传 false。 |
role |
Query enum,可选 | 按现有角色筛选:owner、admin、member。例如管理员列表传 role=admin。 |
eligible_role |
Query enum,可选 | 候选角色视图:admin 表示添加管理员候选,owner 表示转让群主候选;响应项会带 eligible、ineligible_reason。 |
mute_eligible |
Query bool,可选 | 禁言候选选择器传 true;响应项会带 eligible、ineligible_reason。该参数用于响应阶段计算候选状态,不改变公共成员页缓存维度。 |
响应重点字段:items[] 返回 user_id,display_name,group_nickname,avatar_file_id,role,mentionable,acs,eligible,ineligible_reason;next_cursor 为空表示没有下一页;permissions.can_mention_all 只表示当前登录用户是否可以使用 @所有人。
典型调用:
| 场景 | 调用方式 | 说明 |
|---|---|---|
| 进入普通群聊 | 不调用成员列表 | 只用群详情、会话上下文和 member_count。 |
| 群成员第一页 | GET /v1/im/groups/{topic}/members?limit=30 |
打开成员列表时拉第一页。 |
| 群成员下一页 | GET /v1/im/groups/{topic}/members?cursor={next_cursor}&limit=30 |
只使用服务端返回的 next_cursor。 |
| 成员搜索 | GET /v1/im/groups/{topic}/members?query=张&limit=30 |
query 变化时清空本地结果,从第一页重新拉。 |
@ 选择器 |
GET /v1/im/groups/{topic}/members?query=张&mentionable=true&limit=30 |
只展示 mentionable=true 的成员;是否展示 @所有人 看 permissions.can_mention_all。 |
| 管理员列表 | GET /v1/im/groups/{topic}/members?role=admin&limit=30 |
群设置页展示管理员。 |
| 添加管理员候选 | GET /v1/im/groups/{topic}/members?eligible_role=admin&query=张&limit=30 |
用 eligible=false 和 ineligible_reason 禁用不可选项。 |
| 转让群主候选 | GET /v1/im/groups/{topic}/members?eligible_role=owner&query=张&limit=30 |
只允许选择符合转让条件的成员。 |
| 禁言候选 | GET /v1/im/groups/{topic}/members?mute_eligible=true&query=张&limit=30 |
管理员选择禁言对象时使用。 |
客户端本地缓存建议按 tenant_code + my_user_id + topic + query + cursor + limit + role + eligible_role + mentionable + mute_eligible 建 key,只缓存已加载页,并且只在当前页面生命周期内复用。收到 group.member.joined、group.member.left、group.member.updated、group.settings.updated 等群业务事件后,应清理该 topic 的本地成员列表、候选人和 @ 搜索缓存;只发生用户资料/备注变化时,可刷新当前可见页或对应用户资料。
服务端以 topic + role + eligible_role + query + cursor + limit + mentionable 作为 Redis 成员页缓存维度,同一页短时间重复请求会命中页缓存。mute_eligible、好友备注、当前查看人关系等 viewer-specific 字段不进入公共页缓存,而是在 HTTP 响应阶段按当前登录人实时补齐。成员加入、退出、角色/昵称/状态变化时按 topic 清理相关页缓存。该接口不承载在线状态,在线/离线应走 presence 或会话状态逻辑。
6.14 群简介.png
状态:同 6.11,需改造。
- 保存调用
PATCH /v1/im/groups/{topic},Body{"description":"...","version":n};空字符串表示清空,省略表示不修改。 - 服务端返回
VALIDATION_FAILED的details.field/max_length或CONTENT_REJECTED;前端保留草稿供修正。
6.15 群标识.png
状态:角色已有基础数据;产品渲染需统一。
- 群消息下行增加服务端确认的
sender_snapshot{display_name,role},角色枚举固定owner|admin|member。 - 成员角色变化事件到达后刷新成员缓存;历史消息可以保留发送时角色快照,详情以当前角色接口为准。
- 客户端不得根据本地管理员列表给消息自行加可信标识。
6.16 群二维码页面.png
状态:新增。
POST /v1/im/groups/{topic}/qrcodes,Body{"expires_in_seconds":86400},返回签名qr_token,qr_payload,expires_at,group,group_snapshot;group与group_snapshot内容一致,前端优先使用group。- 群不存在返回
404/30004 RESOURCE_NOT_FOUND;群已解散返回410/30004 GROUP_DISSOLVED;非群主/管理员返回403/30005 PERMISSION_DENIED。通用语义见 0.4。 - 扫码调用
POST /v1/im/qrcodes/resolve,purpose:"join_group";随后POST /v1/im/groups/{topic}/join-requests并携带qr_token。 - 群解散、二维码过期或管理员撤销后必须失效;二维码不得直接承载内部群权限。
6.17 群组管理.png
状态:新增。
- 查询/更新:
GET/PATCH /v1/im/groups/{topic}/governance,字段为join_approval,mute_all,member_upload_files,member_private_chat,member_invite和version。 - 每个字段返回
editable;PATCH 仅群主或被授权管理员可用,记录操作者、旧值、新值和 Request ID。 - 解散:
DELETE /v1/im/groups/{topic},需二次X-Action-Token和幂等键;成功后撤销 Topic、二维码、邀请和文件新增权限。 - 群不存在返回
404/30004 RESOURCE_NOT_FOUND;已解散重复解散返回410/30004 GROUP_DISSOLVED;非群主返回403/30005 PERMISSION_DENIED。通用语义见 0.4。
6.18 设置群管理员.png
状态:新增。
GET /v1/im/groups/{topic}/members?role=admin&cursor=返回管理员列表;GET /v1/im/groups/{topic}/role-limits返回max_admin_count,current_admin_count。- 只有群主可进入。群主本人以
owner单独展示,不计入普通管理员列表和名额。
6.19 添加管理员.png
状态:新增。
- 候选:
GET /v1/im/groups/{topic}/members?eligible_role=admin&query=&cursor=,返回eligible和不可选原因。 - 提交:
POST /v1/im/groups/{topic}/admins/batch,Body{"user_ids":[],"version":n};服务端原子校验群主身份、成员资格和管理员上限。 - 超限返回
GROUP_ADMIN_LIMIT_EXCEEDED,并附max/current/requested。
6.20 添加管理员-设置成功.png
状态:新增。
- 6.19 成功返回
admins,version,updated_at;前端用响应替换列表并显示成功,不在请求完成前自行修改角色。 - 服务端推送
group.role.updated,新管理员收到后刷新菜单权限和群标识。
6.21 添加管理员-设置成功(1).png
状态:新增。
- 提交期间按幂等键禁重复点击。批量接口返回
succeeded[]、failed[{user_id,reason}]和最新admins。 - 部分失败时保留成功角色,不做整批客户端回滚;前端展示失败原因并以服务端列表为准。
6.22 删除管理员-设置成功.png
状态:新增。
DELETE /v1/im/groups/{topic}/admins/{user_id},带幂等键和If-Match/version。- 成功后目标角色变为
member,保留群成员关系;服务端立即撤销管理权限并广播角色事件。 - 删除群主、非成员或最后状态冲突分别返回稳定错误,不允许前端拼接通用成功。
6.23 添加禁言成员.png
状态:新增。
- 候选:
GET /v1/im/groups/{topic}/members?mute_eligible=true&query=&cursor=。 - 提交:
POST /v1/im/groups/{topic}/mutes/batch,Body{"members":[{"user_id":"usr...","expires_at":null}],"reason":""}。 - 服务端校验操作者角色层级;管理员不能禁言群主,普通管理员互相操作规则由治理策略确定。
6.24 禁言的群成员.png
状态:新增。
GET /v1/im/groups/{topic}/mutes?cursor=返回user,muted_by,muted_at,expires_at;解除DELETE /v1/im/groups/{topic}/mutes/{user_id}。- 禁言成员发送消息时服务端返回
GROUP_MUTED和可选expires_at;不能只禁用前端输入框。 - 退群时清理禁言记录;过期由服务端自动失效并推送更新。
6.25 选择新群主.png
状态:新增。
GET /v1/im/groups/{topic}/members?eligible_role=owner&query=&cursor=,只返回状态正常的当前成员。- 前端单选并保存
target_user_id;当前群主、已退出、停用账号返回eligible=false。 - 此步骤不修改权限,真正转让仅在 6.26 确认后执行。
6.26 选择新群主提示.png
状态:新增。
POST /v1/im/groups/{topic}/owner-transfer,Body{"target_user_id":"usr...","version":n},必须带幂等键和二次认证X-Action-Token。- 服务端事务内把旧群主降为管理员或成员、目标升为群主、更新权限和审计,再生成
group.owner.transferred群业务事件。 - 成功返回
owner,previous_owner,roles,version;并发成员变化返回GROUP_VERSION_CONFLICT,前端重新确认,不自动重试。
6.27 群文件.png
状态:新增群文件索引;底层文件存储已实现。
GET /v1/im/groups/{topic}/files?query=&type=&cursor=返回group_file_id,file_id,name,size,mime,uploader,created_at,download_state,permissions。- 下载使用认证文件接口,服务端校验当前群成员和文件状态;删除
DELETE /v1/im/groups/{topic}/files/{group_file_id}需上传者或管理员权限。 - 退群后不可继续使用旧 URL 下载;缓存文件是否保留由客户端隐私策略处理。
6.28 群文件-上传状态.png
状态:需改造。
- 先用
purpose=group_file,topic=...上传,再POST /v1/im/groups/{topic}/files,Body{"file_id":"...","file_name":"方案.pdf","mime_type":"application/pdf","file_size":1024}建立群文件记录。 - 服务端校验
member_upload_files、文件是否已上传完成、file_name长度不超过 255、mime_type长度不超过 128、file_size非负且与上传记录一致、单文件上限、租户总存储和群成员身份;成功返回完整群文件项。 - 上传失败按整文件重传重新上传并取得新
file_id;登记失败不得在列表显示,孤立文件按 TTL 回收。
6.29 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
6.29.1 群聊、群成员与群治理
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/groups?category=owned&query=&cursor= |
Query only | {"items":[{"topic":"grp...","name":"项目群","avatar_file_id":"","member_count":3,"my_role":"owner","updated_at":"2026-08-20T09:00:00Z"}],"next_cursor":"","has_more":false} |
GET /v1/im/groups?category=owned|managed|joined&query=&cursor= |
Query only | {"items":[{"topic":"grp...","name":"项目群","avatar_file_id":"","member_count":3,"my_role":"admin","updated_at":"2026-08-20T09:00:00Z"}],"next_cursor":"","has_more":false} |
POST /v1/im/groups |
{"name":"项目群","description":"项目沟通","member_user_ids":["usr..."],"avatar_file_id":""} |
{"topic":"grp...","name":"项目群","avatar_file_id":"","description":"项目沟通","owner":"usr_me","member_count":2,"version":1,"my_role":"owner","permissions":{"can_edit_profile":true,"can_invite_members":true,"can_manage_members":true,"can_manage_settings":true,"can_leave":false}} |
GET /v1/im/groups/{topic} |
Path topic | {"topic":"grp...","name":"项目群","avatar_file_id":"","description":"项目沟通","owner":"usr_me","member_count":2,"version":1,"my_role":"owner","permissions":{"can_edit_profile":true}} |
PATCH /v1/im/groups/{topic} |
{"name":"新群名","avatar_file_id":"file_avatar","description":"新简介","version":1} |
{"topic":"grp...","name":"新群名","avatar_file_id":"file_avatar","description":"新简介","version":2,"permissions":{"can_edit_profile":true}} |
DELETE /v1/im/groups/{topic} |
Header X-Action-Token: action_token;Body {} |
{"topic":"grp...","deleted":true,"status":"dissolved","version":2} |
GET /v1/im/groups/{topic}/settings |
Path topic | {"topic":"grp...","profile":{"name":"项目群","description":"项目沟通"},"pinned":false,"mute_notifications":false,"member_count":2,"permissions":{"can_manage_settings":true},"governance":{"join_approval":false,"mute_all":false,"member_invite":true}} |
PATCH /v1/im/groups/{topic}/member-settings |
{"pinned":true,"mute_notifications":false,"version":1} |
{"topic":"grp...","pinned":true,"mute_notifications":false,"version":2,"updated_at":"2026-08-20T09:00:00Z"} |
PUT /v1/im/groups/{topic}/members/me/profile |
{"nickname":"我在群里的昵称","version":1} |
{"nickname":"我在群里的昵称","version":2,"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/groups/{topic}/leave |
{} |
{"topic":"grp...","status":"left"} |
GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true |
Query only | {"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","group_nickname":"小张","avatar_file_id":"","role":"member","member_status":"active","version":1,"joined_at":"2026-08-20T09:00:00Z","updated_at":"2026-08-20T09:00:00Z","mentionable":true,"acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}}],"next_cursor":"","sync_mode":"full","permissions":{"can_mention_all":true}} |
GET /v1/im/groups/{topic}/members/{user_id} |
Path topic/user | {"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","member_status":"active","version":1,"joined_at":"2026-08-20T09:00:00Z","updated_at":"2026-08-20T09:00:00Z","sync_mode":"single","acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}} |
DELETE /v1/im/groups/{topic}/members/{user_id} |
Path topic/user;管理员可移除普通成员,群主可移除管理员和普通成员;不能移除群主或自己 | {"topic":"grp...","user_id":"usr...","status":"left"} |
GET /v1/im/groups/{topic}/members/{user_id}/profile |
Path topic/user;topic 必须是 grp...,不能传 usr...;user_id 可传本人,返回 relationship:"self"、capabilities.can_send=true |
{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}} |
POST /v1/im/groups/{topic}/qrcodes |
{"expires_in_seconds":86400} |
{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group":{"topic":"grp...","name":"项目群","member_count":2},"group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}} |
POST /v1/im/groups/{topic}/invitations |
{"user_ids":["usr..."],"message":"进群一起讨论"} |
{"topic":"grp...","items":[{"user_id":"usr...","status":"joined","invitation_id":"ginv_1"}]} |
POST /v1/im/group-invitations/{invitation_id}/accept 待实现 |
{} |
{"invitation_id":"ginv_1","status":"joined","topic":"grp..."} |
POST /v1/im/groups/{topic}/join-requests |
{"message":"申请入群","source":"qr","qr_token":"token..."} |
{"request_id":"gjr_1","topic":"grp...","status":"pending","created_at":"2026-08-20T09:00:00Z","version":1} |
GET /v1/im/groups/{topic}/join-requests?status=pending&cursor= |
Query only | {"topic":"grp...","items":[{"request_id":"gjr_1","user_id":"usr...","display_name":"张三","avatar_file_id":"avatar.jpg","user":{"user_id":"usr...","display_name":"张三","avatar_file_id":"avatar.jpg"},"inviter_id":"usr_inviter","inviter_display_name":"李四","inviter_avatar_file_id":"inviter.jpg","inviter":{"user_id":"usr_inviter","display_name":"李四","avatar_file_id":"inviter.jpg"},"status":"pending","source":"invitation","created_at":"2026-08-20T09:00:00Z","version":1}],"next_cursor":""} |
POST /v1/im/groups/{topic}/join-requests/{id}/accept |
{"version":1} |
{"request_id":"gjr_1","topic":"grp...","status":"accepted","handled_at":"2026-08-20T09:00:00Z","version":2} |
POST /v1/im/groups/{topic}/join-requests/{id}/reject |
{"version":1,"reason":"人数已满"} |
{"request_id":"gjr_1","topic":"grp...","status":"rejected","handled_at":"2026-08-20T09:00:00Z","version":2} |
GET /v1/im/groups/{topic}/summary |
Path topic | {"topic":"grp...","pending_join_request_count":0,"announcement":{"announcement_id":"ann_1","content":"公告"},"permissions":{"can_manage_members":true}} |
GET /v1/im/groups/{topic}/announcements/latest |
Path topic | {"announcement_id":"ann_1","topic":"grp...","content":"公告内容","version":1,"published_at":"2026-08-20T09:00:00Z","permissions":{"can_edit":true}} |
PUT /v1/im/groups/{topic}/announcements/current |
{"content":"新的公告","version":1} |
{"announcement_id":"ann_1","topic":"grp...","content":"新的公告","version":2,"published_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/groups/{topic}/announcements/{announcement_id} |
Path announcement | {"announcement_id":"ann_1","topic":"grp...","content":"公告内容","version":2,"published_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/groups/{topic}/announcements/{id}/read |
{} |
{"announcement_id":"ann_1","read_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/groups/{topic}/governance |
Path topic | {"join_approval":false,"mute_all":false,"member_upload_files":true,"member_private_chat":true,"member_invite":true,"version":1} |
PATCH /v1/im/groups/{topic}/governance |
{"join_approval":true,"member_invite":false,"version":1} |
{"join_approval":true,"member_invite":false,"version":2} |
GET /v1/im/groups/{topic}/role-limits |
Path topic | {"max_admin_count":10,"current_admin_count":1} |
GET /v1/im/groups/{topic}/members?role=admin&cursor= |
Query only | {"topic":"grp...","items":[{"user_id":"usr_admin","display_name":"管理员","role":"admin"}],"next_cursor":""} |
GET /v1/im/groups/{topic}/members?eligible_role=admin&query=&cursor= |
Query only | {"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","role":"member","eligible":true}],"next_cursor":""} |
POST /v1/im/groups/{topic}/admins/batch |
{"user_ids":["usr..."],"version":1} |
{"succeeded":[{"user_id":"usr...","role":"admin"}],"admins":[{"user_id":"usr...","role":"admin"}],"version":2,"updated_at":"2026-08-20T09:00:00Z"} |
DELETE /v1/im/groups/{topic}/admins/{user_id} |
Header If-Match: 2 或 Body version |
{"user_id":"usr...","role":"member","updated_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/groups/{topic}/members?mute_eligible=true&query=&cursor= |
Query only | {"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","eligible":true}],"next_cursor":""} |
POST /v1/im/groups/{topic}/mutes/batch |
{"members":[{"user_id":"usr...","expires_at":null}],"reason":"刷屏"} |
{"topic":"grp...","items":[{"user_id":"usr...","muted":true,"muted_at":"2026-08-20T09:00:00Z","expires_at":null}]} |
GET /v1/im/groups/{topic}/mutes?cursor= |
Query only | {"topic":"grp...","items":[{"user":{"user_id":"usr...","display_name":"张三"},"muted_by":"usr_admin","muted_at":"2026-08-20T09:00:00Z","expires_at":null}],"next_cursor":""} |
DELETE /v1/im/groups/{topic}/mutes/{user_id} |
Path user | {"user_id":"usr...","muted":false} |
GET /v1/im/groups/{topic}/members?eligible_role=owner&query=&cursor= |
Query only | {"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","eligible":true}],"next_cursor":""} |
POST /v1/im/groups/{topic}/owner-transfer |
Header X-Action-Token: action_token;Body {"target_user_id":"usr...","version":2} |
{"owner":"usr...","previous_owner":"usr_old","version":3} |
GET /v1/im/groups/{topic}/files?query=&type=&cursor= 待修复 |
Query only | {"topic":"grp...","items":[{"group_file_id":"gfile_1","file_id":"file_1","name":"方案.pdf","size":1024,"mime":"application/pdf","uploader":{"user_id":"usr...","display_name":"张三"},"created_at":"2026-08-20T09:00:00Z","download_state":"available","permissions":{"can_delete":true}}],"next_cursor":""} |
POST /v1/im/groups/{topic}/files 待修复 |
{"file_id":"file_1"} |
{"group_file_id":"gfile_1","file_id":"file_1","topic":"grp...","created_at":"2026-08-20T09:00:00Z"} |
DELETE /v1/im/groups/{topic}/files/{group_file_id} 待修复 |
Path group file ID | {"group_file_id":"gfile_1","deleted":true} |
7. 文件下载、收藏管理与聊天记录检索
7.1 我的收藏.png
状态:新增。
GET /v1/im/favorites?type=all|text|image|video|voice|file|link&q=&cursor=,返回favorite_id,type,snapshot,source,collected_at,availability。- 类型与关键词由服务端过滤,客户端不下载全部收藏再筛选。
source仅下发用户仍可知晓的会话摘要。 - 收藏数据属于当前租户当前用户;跨租户登录不得合并列表。
7.2 我的收藏(1).png
状态:新增。
- 详情:
GET /v1/im/favorites/{favorite_id};删除:DELETE /v1/im/favorites/{favorite_id};转发使用 7.8。 - 收藏创建时保存受控快照和附件引用,因此原消息对当前用户删除后仍可按保留策略展示;原资源因合规删除时返回
availability:"removed"。 - 删除收藏不删除原消息或已转发消息。
7.3 我的收藏-文件.png
状态:新增。
- 调用 7.1,固定
type=file;项中使用snapshot.file{name,size,mime,file_id}和availability。 - 点击前调用收藏详情获得当前授权,下载仍走 7.9 文件协议;失效显示原因,不反复请求公共 URL。
7.4 我的收藏-文本.png
状态:新增。
- 调用 7.1,固定
type=text;服务端返回纯文本/结构化 Drafty 快照和安全高亮区间matches[]。 - 展开、折叠、复制为本地动作;复制前遵守企业 DLP 策略
permissions.can_copy。
7.5 我的收藏-语音.png
状态:新增。
- 调用 7.1,固定
type=voice;播放前获取认证文件,返回duration_ms,waveform,availability。 - 播放/暂停和单实例播放器是客户端状态;服务端不接收逐秒播放进度,除非后续定义跨端收听状态。
7.6 我的收藏—链接.png
状态:新增。
- 调用 7.1,固定
type=link;服务端快照返回url,title,summary,thumbnail_file_id,risk_level。 - 点击前
POST /v1/im/security/urls/check,Body{"url":"...","source":"favorite"};返回allow|warn|block和risk_challenge_id。 - 客户端不得在未确认时自动打开或预加载高风险外链。
7.7 收藏—文件(多选).png
状态:新增。
- 选择状态本地维护;批量删除
POST /v1/im/favorites/batch-delete,Body{"favorite_ids":[]};批量转发见 7.8。 - 返回逐项成功/失败;删除必须二次确认并带幂等键。跨页选择只保存 ID,不保留失效的临时下载地址。
7.8 收藏文件—分享给好友页面.png
状态:新增。
- 目标列表使用 4.17;提交
POST /v1/im/favorites/forward,Body{"favorite_ids":[],"target_topics":[],"comment":""}。 - 服务端从收藏的受控附件引用生成新消息和新 ACL,返回每个目标的消息结果;收藏原件不变。
- 文件失效、目标禁言或无发送权限按目标逐项失败,不允许客户端直接复用旧下载 URL 发送。
7.9 文件-下载.png
状态:下载授权已实现(按上传者/会话成员/消息引用/群成员/好友/收藏放行)。
- 客户端使用
GET /v0/file/s/{file_id},Header 传 API Key 与X-MeetingIM-Auth: token <base64_token>;禁止?auth=&secret=。URL 可带扩展名,服务端自动剥离。 - 服务端按文件 ACL 放行:上传者本人、文件所属
p2p/grp会话成员、消息级引用可见者(未撤回未删除且未被执行删除)、群成员、好友关系、收藏引用;非成员返回403 permission denied(FILE_FORBIDDEN)。对象存储模式下鉴权通过返回 302 短期签名 URL。 - 前提:发送方上传附件时带了
topic(见 0.5.5.1)。若对端下载一直 404,说明该文件从未关联会话/消息且已被 1 小时回收任务删除,应让发送方重新上传并携带topic。 - 下载前客户端校验本地空间和网络策略;
FILE_FORBIDDEN不自动重试,FILE_UNAVAILABLE显示失效。
7.10 文件-下载中.png
状态:客户端任务管理;服务端需支持 Range。
- 下载任务保存
file_id,etag,total_bytes,downloaded_bytes,temp_path,state;恢复时携带Range与If-Range: <etag>。 - ETag 改变或服务端返回完整 200 时丢弃旧分片重新下载;取消时关闭连接并清理临时文件。
- 下载中断后,如果客户端通过有效 Token 登录取得了新 Token,则使用新 Token 和原 Range 状态续传;Token 已过期或失效时暂停任务并返回登录页。不能把 Token 写入任务文件或日志。
7.11 文件-下载完成.png
状态:纯客户端完成态。
- 完成后校验长度和服务端提供的 checksum,再原子移动到受控目录;校验失败删除并标记重试。
- 打开/分享遵守系统沙箱和企业 DLP。每次展示页面检查本地文件存在性,不存在则恢复“可下载”。
7.12 查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容).png
状态:新增服务端搜索。
GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor=返回seq,from,ts,snippet,matches[]。- 点击结果后
GET /v1/im/topics/{topic}/messages/{seq}/context?before=20&after=20,返回锚点及上下文;客户端定位seq并高亮。 - 服务端只搜索当前用户可见且未硬删除的消息,关键词做长度/频率限制,搜索日志不得记录原文。
- 搜索匹配字段包括文本正文、
content.text/txt/content/title/summary/name、文件名、链接标题/摘要、卡片/转发快照等可展示文本;排序按seq desc,next_cursor继续向更早消息翻页。
7.13 查找聊天记录-文件.png
状态:新增。
- 调用
GET /v1/im/topics/{topic}/messages/search?type=file&q=&cursor=,返回消息seq与文件快照、发送者、时间、availability。 - 预览/下载使用 7.9;撤回或无授权时显示失效。点击来源用 7.12 上下文接口定位。
7.14 查找聊天记录-链接.png
状态:新增。
- 调用搜索接口
type=link,返回url,title,summary,seq,ts,risk_level;打开前执行 7.6 URL 安全检查。 - 链接预览由服务端受控抓取和缓存,客户端不直接抓取内网地址,防止 SSRF 和隐私泄露。
7.15 查看聊天记录-图片.png
状态:新增。
GET /v1/im/topics/{topic}/media?type=image&cursor=返回seq,thumbnail_file_id,file_id,width,height,ts,availability。- 缩略图懒加载使用认证文件请求;大图前再次授权。多选仅保存
(topic,seq),操作使用第 4 章批量接口。
7.16 查看聊天记录-视频.png
状态:新增。
- 调用 7.15 接口,
type=video,额外返回duration_ms,mime,size。 - 播放器使用支持 Range 的认证下载/流式读取;文件失效、Token 过期和网络中断分别映射可理解状态,返回列表保持游标与滚动位置。
7.17 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
7.17.1 收藏、转发与文件安全
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/favorites?type=all&q=&cursor= |
Query only | {"items":[{"favorite_id":"fav_1","type":"text","snapshot":{"text":"收藏内容"},"source":{"topic":"grp...","seq":12},"collected_at":"2026-08-20T09:00:00Z","availability":"available"}],"next_cursor":""} |
GET /v1/im/favorites?type=all|text|image|video|voice|file|link&q=&cursor= |
Query only | {"items":[{"favorite_id":"fav_1","type":"image","snapshot":{"thumbnail_file_id":"file_thumb","file_id":"file_img"},"source":{"topic":"grp...","seq":12},"collected_at":"2026-08-20T09:00:00Z","availability":"available"}],"next_cursor":""} |
POST /v1/im/favorites/batch |
{"messages":[{"topic":"grp...","seq":"12"}]} |
{"created_count":1,"existing_count":0,"failed":[]} |
GET /v1/im/favorites/{favorite_id} |
Path favorite ID | {"favorite_id":"fav_1","type":"file","snapshot":{"file_id":"file_1","name":"a.pdf"},"source":{"topic":"grp...","seq":12},"availability":"available"} |
DELETE /v1/im/favorites/{favorite_id} |
Path favorite ID | {"favorite_id":"fav_1","deleted":true} |
POST /v1/im/favorites/batch-delete |
{"favorite_ids":["fav_1","fav_2"]} |
{"items":[{"favorite_id":"fav_1","deleted":true},{"favorite_id":"fav_2","deleted":true}],"failed":[]} |
POST /v1/im/favorites/forward |
{"favorite_ids":["fav_1"],"target_topics":["grp..."],"comment":"转给你"} |
{"items":[{"favorite_id":"fav_1","target_topic":"grp...","status":"sent","seq":20,"comment_seq":19,"comment_status":"sent"}],"failed":[]};Kafka 投递结果不确定时返回 status:"pending"、reason:"MESSAGE_PERSIST_PENDING" |
GET /v1/im/forward-targets?query=&cursor= |
Query only | {"items":[{"topic":"grp...","type":"group","display_name":"项目群","avatar_file_id":"","can_receive_forward":true},{"topic":"slf","type":"file_assistant","display_name":"文件助手","can_receive_forward":true}],"next_cursor":"","has_more":false} |
POST /v1/im/messages/forward 逐条转发 |
{"mode":"separate","sources":[{"topic":"grp...","seq":12}],"target_topics":["usr..."],"comment":"转给你"} |
{"items":[{"source":{"topic":"grp...","seq":12},"target_topic":"p2p...","client_mid":"mid_1","seq":30,"status":"sent","comment_seq":29,"comment_status":"sent"}],"failed":[]};Kafka 投递结果不确定时返回 status:"pending"、reason:"MESSAGE_PERSIST_PENDING" |
POST /v1/im/messages/forward 合并转发 |
{"mode":"bundle","sources":[{"topic":"grp...","seq":12},{"topic":"grp...","seq":13}],"target_topics":["usr..."],"comment":"转给你"} |
{"items":[{"sources":[{"topic":"grp...","seq":12},{"topic":"grp...","seq":13}],"target_topic":"p2p...","client_mid":"mid_1","seq":31,"status":"sent","mode":"bundle","bundle_id":"fbundle_1","comment_seq":30,"comment_status":"sent"}],"failed":[]};Kafka 投递结果不确定时返回 status:"pending"、reason:"MESSAGE_PERSIST_PENDING" |
GET /v1/im/forward-bundles/{bundle_id}?cursor= 待实现 |
Query only | {"bundle_id":"fbundle_1","title":"聊天记录","item_count":2,"items":[{"from_display":"张三","ts":"2026-08-20T09:00:00Z","summary":"文本摘要"}],"next_cursor":""} |
POST /v1/im/security/urls/check |
{"url":"https://example.com","source":"favorite"} |
{"url":"https://example.com","decision":"allow","risk_level":"low","reason":"ok","source":"favorite"} |
8. 实时语音与视频通话
8.1 各种通话提示状态.png
状态:已实现基础通话记录替换;产品文案映射需统一。
- 发起呼叫时的持久消息就是通话记录。服务端以相同
seq写 replacement{data},head.replace=":{seq}",并把head.webrtc更新为accepted|finished|disconnected|missed|declined。 - 已接通结束时
head.webrtc-duration为服务端计算的毫秒数。客户端映射:finished=已结束、missed=未接听/已取消、declined=已拒绝、disconnected=异常结束。 - 双方使用原消息
content.callId/callType和同一seq;客户端不得另发一条“通话结果”消息或相信本地传入的 duration。
8.2 对方状态.png
状态:在线邀请、短有效期普通 Push 与产品鉴权已实现。
- 来电是 Topic 下行的初始
{data}:head.webrtc="started"、head.x-im-type="call",content至少含callId,callType,audio,video。客户端保存该消息seq作为后续所有 call note 的关联 ID。 - 来电端开始响铃后发送
{note:{topic,what:"call",event:"ringing",seq,payload:{"callId":"..."}}};接听发送event:"accept";拒绝发送event:"hang-up"。 - 后台 Push 只携带
callId,topic,seq,caller_id,caller_display,callType,expires_at,不含 SDP、ICE 或 Token;iOS 来电 Push 使用普通 alert,category=im_call,content-available=1,短 TTL 约 30 秒,不使用 PushKit VoIP。 - 被叫在未发送
ringing/accept前也可以直接发送hang-up,服务端按topic+seq+callId校验后立即结束主叫侧振铃,并把原通话消息替换为declined;主叫主动取消则替换为missed。
8.3 语音通话-对方待接受状态.png
状态:发起与超时已实现基础;好友/拉黑/租户开关校验需改造。
- 音视频入口先看
GET /v1/im/bootstrap的features.audio_call/video_call和permissions.audio_call/video_call;这两个值来源于 Admin 后台现有配置读取链路im_client_feature分组中的audio_call_enabled、video_call_enabled,与声网参数使用同一个配置读取方法,但不放在声网分组。具体到某个会话或用户时,再看capabilities.can_call。任一为false时前端不展示对应语音/视频按钮。 - 发起前查询
capabilities.can_call,再发布持久邀请消息:
{"pub":{"id":"call-1","topic":"p2p...","head":{"mime":"application/json","webrtc":"started","x-im-type":"call"},"content":{"callId":"01K...","callType":"audio","audio":true,"video":false}}}
{ctrl code:202,params.seq}的seq是后续ringing/accept/offer/answer/ice-candidate/hang-up必填关联值。服务端只允许 P2P 双方参与,并校验好友、拉黑、租户通话开关和当前 Topic 无并发呼叫。- 发起方取消发送
{note event:"hang-up",seq,payload:{"callId":"...","reason":"cancelled"}};等待超时由服务端结束并把原消息替换为missed。
8.4 语音通话-双方语音沟通中.png
状态:WebRTC 客户端能力;信令和服务端计时已实现。
- 被叫发送
accept后,服务端把原消息替换为webrtc:"accepted"并向主叫发送{info what:"call",event:"accept",seq}。 - 双方随后用
{note what:"call",event:"offer|answer|ice-candidate",seq,payload:{"callId":"...",...}}交换 SDP/ICE。ICE 配置使用hi.ctrl.params.iceServers,不得在 App 中硬编码 TURN 长期凭证。 - 任一端发送
hang-up,服务端按接受时间计算 duration 并替换原通话消息。异常断线后按原topic+seq+callId恢复或结束,不新建第二通呼叫。
8.5 语音通话-麦克风与扬声器已关.png
状态:纯客户端设备控制。
- 麦克风关闭通过禁用本地音轨,扬声器关闭/听筒切换通过系统音频路由完成,不调用业务 HTTP。
- UI 必须读取实际音轨和路由状态;权限撤销时强制更新。是否告知对方静音可发送临时
media-state,不落聊天记录。
8.6 语音通话-麦克风与扬声器已开.png
状态:纯客户端设备控制。
- 开启麦克风恢复本地音轨,开启扬声器选择实际输出设备;蓝牙/耳机变化以操作系统回调为准。
- 不把设备名称、系统音量上传服务端。切换失败恢复真实按钮状态并给本地提示。
8.7 视频通话—等待对方接受接受邀请中.png
状态:基础通话信令已实现;隐私约束需落实。
- 发起协议同 8.3,
media_type:"video"。本地预览只在设备显示,接听前媒体轨默认不发给对方,SDP 只声明能力。 - 切换摄像头纯客户端;取消发送
hang-up:cancelled。相机权限不足时改为音频呼叫或取消,不伪装视频已开启。
8.8 视频通话—等待对方接受接受邀请-摄像头关闭.png
状态:纯客户端状态;媒体状态事件需改造。
- 关闭本地视频轨并显示头像;接听后沿用
video_enabled=false。 - 当前服务端只允许
ringing/accept/offer/answer/ice-candidate/hang-up;首版应直接通过 WebRTC 轨道 mute 状态呈现。若后续新增media-state,必须先修改server/calls.go事件白名单、参与者校验和限频,客户端不能提前发送未知事件。
8.9 双方视频通话中.png
状态:WebRTC 客户端能力;服务端信令已具备基础。
- 主/小窗布局、拖动和横竖屏纯客户端;WebRTC offer/answer/ICE 与 8.2~8.4 相同。
- 服务端只做身份鉴权、信令路由、TURN 配置和通话审计,不接收前端伪造的通话时长。
- 网络恶化可本地降码率或关闭视频轨;最终结束必须发
hang-up。服务端未支持media-state前不得发送该事件。
8.10 双方视频通话中-对方关闭摄像头状态.png
状态:现阶段纯客户端;可选媒体状态事件需改造。
- 以对端 WebRTC 视频轨的 mute/unmute 作为真实信号;关闭时显示资料缓存中的对方头像并保留音频,轨道恢复后自动显示视频。
- 不需要业务 HTTP。可选
media-state必须等服务端扩展事件白名单后另行启用,且不持久化、不改变通话结果。
8.11 视频与语音通话悬浮状态中.png
状态:纯客户端。
- 最小化不结束 WebRTC 或 WebSocket,会话仍以同一
call_id运行;点击恢复全屏不发送新 invite。 - 系统悬浮窗权限、画中画和安全区域按平台实现。收到
hang-up、Token 失效或媒体会话结束后立即关闭悬浮窗并落正确结果态。
8.12 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
8.12.1 实时通话与通话记录
| 接口 | 输入示例 | 成功输出示例 |
|---|---|---|
| WebSocket 发起通话 | {"pub":{"id":"call-1","topic":"p2p...","head":{"mime":"application/json","webrtc":"started","x-im-type":"call"},"content":{"callId":"call_1","callType":"audio","audio":true,"video":false}}} |
{"ctrl":{"id":"call-1","code":202,"params":{"seq":12}}} |
| WebSocket 通话事件 | {"note":{"topic":"p2p...","what":"call","event":"accept","seq":12,"payload":{"callId":"call_1"}}} |
对端收到 {"info":{"topic":"p2p...","what":"call","event":"accept","seq":12,"payload":{"callId":"call_1"}}} |
POST /v1/im/calls/rtc-join |
{"topic":"usr_peer 或 p2p...","call_id":"call_1","seq":12,"call_type":"audio","role":"caller"} |
{"provider":"agora","auth_mode":"token","app_id":"声网 App ID","channel_name":"call_call_1","uid":"usr...","token":"007eJx...","token_expires_at":"2026-09-02T10:00:00Z","call_id":"call_1","topic":"p2p...","seq":12,"call_type":"audio","role":"caller"} |
GET /v1/im/calls?limit=20&cursor= |
Query only | {"items":[{"call_id":"call_1","topic":"p2p...","seq":12,"call_type":"audio","status":"finished","started_at":"2026-08-20T09:00:00Z","duration_ms":60000,"peer":{"user_id":"usr...","display_name":"张三"}}],"next_cursor":""} |
GET /v1/im/calls/{topic}?cursor= |
Path topic | {"topic":"p2p...","items":[{"call_id":"call_1","seq":12,"status":"missed","call_type":"video","created_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
声网模式下,新客户端在发起方收到 ctrl.params.seq 后、被叫方点击接听后,分别调用 POST /v1/im/calls/rtc-join 获取 app_id/channel_name/uid/token。topic 可直接传当前会话里看到的 P2P 别名,例如发起方传被叫 usr...、被叫方传发起方 usr...;后端会归一到真实 p2p... 并在响应中返回。当前按正式 token 模式接入,后端使用 im_shengwang 分组中的 agora_app_certificate 生成 RTC Token。客户端不接触 app_certificate、customer_id、customer_secret,也不再通过 IM WebSocket 交换 SDP/ICE,音视频媒体协商和传输由声网 RTC SDK 完成。
9. 企业工作台与办公协同
9.1 工作台.png
状态:新增。
GET /v1/im/workbench返回modules[{code,name,icon_file_id,route,sort_order,unread_count,enabled,permission}]、布局版本和企业配置;服务端按sort_order ASC返回,默认顺序为打卡、月报、日报、部门框架、举报信箱、会议通知、会议笔记。meetings模块的unread_count由服务端按未读系统通知实时补齐。GET /v1/im/bootstrap返回entry_badges.workbench_total_unread,用于工作台一级入口总红点;当前只汇总“会议通知”模块的未读通知,会议笔记、日报、月报、打卡、部门框架和举报信箱均不产生工作台通知数。- 服务端按当前 Token 的租户、组织角色和功能开关裁剪;前端只接受白名单内部
route,禁止直接执行服务端任意 URL。 - 工作台一级总未读以
GET /v1/im/bootstrap和GET /v1/im/workbench返回为准;当前只有会议通知可接收移动端通知,其他工作台模块在进入页面时主动拉取数据刷新。若后续启用workbench.badge.updated,应按 0.5.4.6 的info.what="im"业务事件下发。切租户必须重新加载,不复用另一企业工作台。
9.2 部门组织框架.png
状态:新增。
- 根节点:
GET /v1/im/organization/departments?parent_id=root&cursor=;子部门同接口传parent_id;成员GET /v1/im/organization/departments/{id}/members?cursor=。 - 部门返回
department_id,name,parent_id,child_count,member_count,version;成员只返回当前查看人可见字段。 - 全局搜索:
GET /v1/im/organization/search?q=&types=department,user&cursor=。服务端执行组织范围授权,离职用户标记不可联系或从结果移除。 - 组织架构可见范围由服务端按当前登录用户裁剪:上架测试账号可看全部组织;手机号注册账号默认只能看到自己与好友所在组织路径及成员;邀请码注册账号默认只能看到自己所在组织路径及本人。上架测试账号通过租户业务库
im_org_visibility_overrides配置visibility_scope=all,客户端不传可见范围参数。
9.2.1 打卡考勤
状态:已实现基础版。
- 打卡规则、班次、位置/Wi-Fi 约束、补卡窗口、异常判定和统计结果必须以后端为真源;客户端只负责展示、采集设备侧上下文和临时离线队列,不能把正式打卡记录或规则只保存在本地。
- 配置:
GET /v1/im/attendance/config?date=2026-08-31返回当前用户当天适用的规则、班次、允许打卡动作和前端展示文案。服务端按租户、部门、人员、节假日和特殊排班裁剪,前端不得自行推导是否迟到/早退/缺卡。 - 记录:
GET /v1/im/attendance/records?from=2026-08-01&to=2026-08-31&scope=mine返回当前用户打卡记录、异常状态和补卡状态;管理员用scope=managed&department_id=&user_id=&cursor=查询权限范围内记录。 - 打卡:
POST /v1/im/attendance/checkins,Body 带date,action,rule_id,shift_id,client_time,location,wifi,device,写操作必须带Idempotency-Key。服务端校验打卡窗口、地理围栏/Wi-Fi、重复提交和设备风险,成功后返回服务端判定状态。 - 补卡/异常申诉:
POST /v1/im/attendance/appeals创建申请,GET /v1/im/attendance/appeals?scope=mine|managed&status=&cursor=查询列表,审批动作由管理端或后续办公审批接口处理。 - 主动下行:规则变更、打卡状态变化、补卡审批变化可通过
info.what="im",事件attendance.rule.updated/attendance.record.updated/attendance.appeal.updated下发;前端收到后刷新配置、记录或列表。
9.3 会议通知-发起人状态.png
状态:新增。
- 创建:
POST /v1/im/meetings,Body{"title":"...","start_at":"...","end_at":"...","location":"...","recorder_user_id":"usr_me","attendee_user_ids":[],"reminder_minutes":[]};recorder_user_id未传时服务端默认使用当前发起人。 - 详情:
GET /v1/im/meetings/{meeting_id}返回会议、参会人的response=pending|accepted|declined|tentative、权限和version。 - 更新/取消:
PATCH /v1/im/meetings/{id}、POST /v1/im/meetings/{id}/cancel;提醒未响应者POST /v1/im/meetings/{id}/reminders。所有写操作带幂等键并生成通知和审计。 - 主动下行:参会人收到
info.what="im",事件为meeting.created/updated/canceled/reminded/response.updated,payload 包含meeting_id,version,state;前端收到后刷新会议列表或会议详情。除操作者外,当前会写入系统通知列表。
9.4 会议通知-接收人状态.png
状态:新增。
- 详情同 9.3;响应:
PUT /v1/im/meetings/{meeting_id}/my-response,Body{"response":"accepted|declined|tentative","version":n}。 - 返回我的最终响应和会议版本,并通过
meeting.response.updated实时通知参会人;重复提交同状态幂等。 - 会议过期返回
MEETING_CLOSED并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。
9.4.1 会议笔记
状态:新增。
- 列表:
GET /v1/im/meeting-notes?query=&cursor=&limit=20返回可查看会议笔记的会议列表、note_status/file_count/permissions。 - 详情:
GET /v1/im/meetings/{meeting_id}/notes返回会议信息和files[]。 - 上传并登记:调用通用文件上传
POST /v0/file/u/,multipart 包含file=@...、purpose=meeting_note、meeting_id=<meeting_id>、id=<本次上传幂等ID>,请求头按文件上传鉴权携带X-MeetingIM-APIKey、X-MeetingIM-Auth。服务端上传成功后会立即登记会议笔记文件,响应ctrl.params包含url,file_id,purpose,meeting_id,note_file_id,note_file_status,version。 - 兼容绑定:若客户端先完成普通文件上传,也可以调用
POST /v1/im/meetings/{meeting_id}/notes/files,Body{"file_id":"..."},并携带Idempotency-Key完成登记。 - 替换/删除:
PUT /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}、DELETE /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}?version=n。只有记录人/发起人按权限可上传、替换和删除。
9.5 日报.png
状态:新增。
GET /v1/im/reports/daily?scope=mine&from=&to=&status=&cursor=返回report_id,report_date,status,draft_updated_at,submitted_at,read_state。- 新建草稿
POST /v1/im/reports/daily,Body{"report_date":"2026-08-11"};同一用户同一天唯一,重复创建返回原草稿。 permissions.can_create和可补交日期由服务端返回,前端不自行判断工作日规则。
9.6 日报-普通用户页面.png
状态:新增。
- 编辑:
PUT /v1/im/reports/daily/{id}/draft,Body{"content":"...","attachments":[],"version":n};提交POST /v1/im/reports/daily/{id}/submit。 - 服务端返回自动保存版本,客户端用 2~5 秒防抖且串行保存;版本冲突返回
REPORT_VERSION_CONFLICT并给最新版本。 - 提交后按
editable_until决定是否可修改;附件使用purpose=daily_report上传并做权限绑定。
9.7 日报-管理员页面.png
状态:新增。
GET /v1/im/reports/daily/statistics?date=&department_id=返回expected_count,submitted_count,missing_count和组织权限范围。- 明细:
GET /v1/im/reports/daily?scope=managed&date=&status=submitted|missing&department_id=&cursor=。 - 提醒:
POST /v1/im/reports/daily/reminders,Body{"date":"...","user_ids":[]};服务端过滤无管理权限对象并限流,返回逐项结果。
9.8 日报-查看详情.png
状态:新增。
GET /v1/im/reports/daily/{report_id}返回填报人、日期、内容、附件、状态、版本、修改时间和permissions。- 管理员查看时服务端记录必要的已读状态;附件下载复用第 7 章 ACL。
- 撤回或修订使用专用状态和版本,客户端必须显示最新版本与
revised_at,不能混合旧缓存。
9.9 月报(1).png
状态:新增。
- 创建/取得当月草稿:
POST /v1/im/reports/monthly,Body{"month":"2026-08"}。 - 保存:
PUT /v1/im/reports/monthly/{id}/draft,Body{"work_summary":"...","next_plan":"...","attachments":[],"version":n};提交POST /v1/im/reports/monthly/{id}/submit。 - 字段必填、最大长度、提交截止时间由响应
validation提供;自动保存和冲突处理同 9.6。
9.10 月报.png
状态:新增。
GET /v1/im/reports/monthly?scope=mine|managed&from_month=&to_month=&status=&cursor=返回月份、填报人、状态和时间。- 管理员统计
GET /v1/im/reports/monthly/statistics?month=&department_id=;普通用户响应中不下发团队统计入口权限。 - 未提交项进入 9.9,已提交项进入 9.11,路由由返回
action=edit|view决定。
9.11 月报-详情页.png
状态:新增。
GET /v1/im/reports/monthly/{report_id}返回month,author,work_summary,next_plan,attachments,status,submitted_at,updated_at,version,permissions。permissions.can_edit=true时进入草稿更新;否则只读。修改已提交月报需专用POST /v1/im/reports/monthly/{id}/reopen或审核流程,不直接覆盖。
9.12 总经理信箱.png
状态:新增。
- 页面配置:
GET /v1/im/executive-mailbox/config返回enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice。 - 提交:
POST /v1/im/executive-mailbox/submissions,Body{"subject":"...","content":"...","anonymous":false,"attachment_file_ids":[]},带幂等键。subject最多 200 字符,content最多 5000 字符;超限返回VALIDATION_FAILED,不返回 500。 - 成功仅返回
submission_id,status:"received",submitted_at;匿名模式下业务接收方不见身份,但安全审计是否保留内部追踪按企业政策执行并提前告知。
9.13 总经理信箱-文案提示.png
状态:新增配置接口。
- 文案使用 9.12 配置中的
notice{title,content,updated_at},由租户后台配置;客户端不得硬编码实名/匿名承诺。 - 提交按钮前必须展示当前版本文案;请求可携带
notice_version,服务端发现关键政策已变更时返回NOTICE_RECONFIRM_REQUIRED。
9.14 会议笔记
会议笔记属于企业工作台模块,复用会议主数据和统一文件能力。服务端负责判断会议是否结束、当前用户是否与会议有关、是否为会议记录员,以及文件查看和操作权限;客户端只根据响应中的状态与 permissions 展示页面,不自行推导权限。
本模块遵循主文档第 0 章的公共约定:产品 HTTP 使用 X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID 和 Authorization: Bearer <token>;写操作额外使用 Idempotency-Key;成功和失败响应均使用统一包裹结构。
会议笔记不产生移动端通知、不写入会议通知未读数,也不增加工作台底部 Tab 红点。客户端进入会议笔记列表或详情页时主动请求最新数据;从上传、预览或下载页面返回后,只刷新当前会议笔记页面。
9.14.1 会议笔记-工作台入口.png
状态:工作台接口需改造。
GET /v1/im/workbench的modules增加code="meeting_notes"、route="/meeting-notes"。服务端按租户功能开关和当前用户权限决定是否返回该入口。- 会议笔记模块固定返回
unread_count=0,不参与GET /v1/im/bootstrap的entry_badges.workbench_total_unread汇总。 - 当前工作台红点只对应“会议通知”模块的未读通知。日报、月报、打卡、部门框架、举报信箱和会议笔记均不产生工作台通知数。
- 客户端点击入口后请求
GET /v1/im/meeting-notes。切换企业后必须清空原租户的会议笔记缓存并重新加载。
9.14.2 会议笔记-列表.png
状态:新增。
- 列表:
GET /v1/im/meeting-notes?query=&cursor=&limit=20。接口只返回当前用户作为发起人、会议记录员或参会人的已结束会议,不返回未开始、进行中或已取消会议。 query支持匹配会议主题、发起人、会议记录员、参会人和地点;搜索范围仍受当前用户会议可见权限限制。- 列表按
end_at倒序排列,分页使用不透明cursor。客户端不得用本地时间过滤会议状态或自行扩大搜索范围。 - 已结束但尚未上传笔记的会议仍返回,使用
note_status="empty"、file_count=0;客户端展示会议基础信息,进入详情后显示“暂无笔记”。 permissions.can_view=true决定是否允许进入详情;permissions.can_upload只对当前会议记录员为true。普通参会人可以进入详情,但不显示上传按钮。state="ended"是服务端按会议状态及服务端当前时间计算或持久化的有效状态。服务端必须以end_at和取消状态为准,不能信任客户端传入的“已结束”条件。
9.14.3 会议笔记-发布页面.png
状态:现有会议创建接口需改造。
- 页面字段顺序为发起人、地点、会议记录员、参会人、开始时间、时长、会议主题。会议记录员默认显示当前会议发起人。
- 创建仍使用
POST /v1/im/meetings,请求新增recorder_user_id。客户端未传或传空字符串时,服务端默认使用当前登录用户;客户端不能通过organizer_id伪造发起人。 - 服务端校验会议记录员属于当前租户且账号有效。首版允许会议记录员是发起人或参会人;若选择了非参会人,服务端自动加入参会人列表并在响应中返回最终名单。
- 服务端保存的会议必须有
recorder_user_id;创建请求可省略该字段以使用发起人默认值。attendee_user_ids、start_at、end_at和title为必填业务数据,location可为空,且end_at必须晚于start_at。 - 创建成功后的会议通知仍统一归属“会议通知”模块;会议笔记模块不额外生成通知或未读数。
PATCH /v1/im/meetings/{meeting_id}支持在会议结束前修改recorder_user_id,并使用version做乐观锁。会议结束后返回MEETING_CLOSED。
9.14.4 会议笔记-详情页.png
状态:新增。
- 详情:
GET /v1/im/meetings/{meeting_id}/notes,一次返回会议基础信息、发起人、会议记录员、参会人、文件列表、笔记状态和当前用户权限。 - 会议基础信息与会议通知页面共用
im_meetings和im_meeting_participants数据,不复制会议标题、时间和成员快照作为另一份可编辑主数据。 - 当前用户为会议记录员且会议已结束时,
permissions.can_upload=true,客户端展示“上传文件”;其他用户不展示该按钮。 - 上传文件先调用
POST /v0/file/u/,Multipart 携带purpose=meeting_note和meeting_id;上传成功取得file_id后,再调用POST /v1/im/meetings/{meeting_id}/notes/files建立会议笔记文件记录。 - 文件登记成功后才进入详情列表。上传成功但登记失败的临时文件不得展示,并由服务端按临时文件 TTL 回收。
- 当前会议记录员可替换或删除自己登记的文件;所有写操作均需
Idempotency-Key,服务端记录操作审计。
9.14.5 会议笔记-详情页(普通成员).png
状态:新增权限响应。
- 普通参会人调用与 9.14.4 相同的详情接口。响应中
permissions.can_view=true、can_upload=false、can_replace=false、can_delete=false。 - 没有已上传文件时返回
note_status="empty"和空数组files=[],客户端展示“暂无笔记”,不把空状态当作请求失败。 - 有文件时普通参会人只展示文件名、大小、类型、上传人、上传时间和预览入口,不展示上传、替换、删除按钮。
- 服务端必须在文件登记、替换、删除、预览和下载接口逐次校验会议关系及角色。隐藏客户端按钮不能代替服务端鉴权。
- 与会议无关、已离开可见范围或跨租户的用户统一返回
RESOURCE_NOT_FOUND或FORBIDDEN,不得通过meeting_id、note_file_id或file_id探测资源。
9.14.6 会议笔记-详情页-在线预览.png
状态:基础文件读取已实现;会议笔记 ACL 和预览转换新增。
- 文件项返回
preview.status=ready|processing|unsupported|failed、preview.mode=native|converted|download_only和可选preview_file_id,客户端根据状态选择直接预览、轮询转换状态或提示暂不支持。 - 原生可预览文件使用受认证的
GET /v0/file/s/{file_id};转换预览使用GET /v0/file/s/{preview_file_id}。对象存储模式下服务端鉴权通过后返回 302 到短期签名 URL,客户端跟随Location获取文件内容。 - 服务端不得返回长期有效的公共下载 URL。客户端缓存键至少包含
tenant_code + user_id + file_id + etag,退出登录或切换企业时清理受控缓存。 preview.status="processing"时客户端可在当前详情页低频刷新,建议 2 秒后开始、最长 30 秒;离开页面立即停止轮询。
9.14.7 会议笔记-详情页-文件下载中.png
状态:复用统一下载任务;会议笔记 ACL 新增。
- 下载使用
GET /v0/file/s/{file_id},Header 传 API Key 与X-MeetingIM-Auth: token <base64_token>,并支持Range和If-Range断点续传;对象存储模式下客户端跟随 302Location。 - 下载任务保存
file_id,etag,total_bytes,downloaded_bytes,temp_path,state。Token 更新后使用新 Token 续传,禁止把 Token 写入任务文件或日志。 - 服务端在每次下载及续传时重新校验会议可见权限。返回
FILE_FORBIDDEN时客户端停止重试;返回FILE_UNAVAILABLE时标记文件失效并刷新详情。 - 下载进度属于客户端本地状态,不更新会议笔记业务状态、不产生工作台通知,也不写入未读数。
9.14.8 会议笔记-文件预览.png
状态:客户端预览页;服务端复用 9.14.6 文件读取协议。
- 预览页展示服务端返回的安全文件名和实际内容;标题不得直接信任未经清理的上传文件名。
- 图片、PDF、文本等受支持类型在应用内预览;Office 等类型优先使用受控转换文件,无法转换时按
preview.mode="download_only"提示下载后打开。 - 文件被删除、审核拒绝、转换失败或权限撤销时,服务端返回稳定
reason,客户端关闭预览并刷新详情,不能继续显示旧的授权链接。 - 从预览页返回详情后调用
GET /v1/im/meetings/{meeting_id}/notes获取最新文件状态。
9.14.9 接口示例
以下示例用于会议笔记 HTTP 联调快速查阅。成功响应统一包裹为 {"code":0,"data":示例,"request_id":"...","server_time":"..."},下表只展示 data。
9.14.9.1 工作台、会议记录员与会议笔记
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/workbench 需改造 |
Query only | {"layout_version":2,"modules":[{"code":"meeting_notifications","name":"会议通知","route":"/meetings","unread_count":3,"enabled":true,"permission":"enabled"},{"code":"meeting_notes","name":"会议笔记","route":"/meeting-notes","unread_count":0,"enabled":true,"permission":"enabled"}]} |
POST /v1/im/meetings 需改造 |
Header Idempotency-Key;Body {"title":"项目周会","start_at":"2026-09-05T10:00:00+08:00","end_at":"2026-09-05T10:30:00+08:00","location":"会议室 A","recorder_user_id":"usr_me","attendee_user_ids":["usr_2"],"reminder_minutes":[10]} |
{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","state":"scheduled","participants":[{"user_id":"usr_me","response":"accepted"},{"user_id":"usr_2","response":"pending"}],"permissions":{"can_update":true,"can_cancel":true},"version":1} |
PATCH /v1/im/meetings/{meeting_id} 需改造 |
Header Idempotency-Key;Body {"recorder_user_id":"usr_2","version":1} |
{"meeting_id":"mtg_1","recorder":{"user_id":"usr_2","display_name":"李四"},"version":2,"updated_at":"2026-09-05T01:30:00Z"} |
GET /v1/im/meeting-notes?query=&cursor=&limit=20 新增 |
Query only | {"items":[{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","location":"会议室 A","state":"ended","participant_count":2,"participant_names":["张三","李四"],"note_status":"empty","file_count":0,"permissions":{"can_view":true,"can_upload":true}}],"next_cursor":"","has_more":false} |
GET /v1/im/meetings/{meeting_id}/notes 新增 |
Path meeting ID | {"meeting":{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","location":"会议室 A","state":"ended","participants":[{"user_id":"usr_me","display_name":"张三"},{"user_id":"usr_2","display_name":"李四"}]},"note_status":"available","files":[{"note_file_id":"mnote_1","file_id":"file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf","uploader":{"user_id":"usr_me","display_name":"张三"},"created_at":"2026-09-05T03:00:00Z","version":1,"preview":{"status":"ready","mode":"native","preview_file_id":"file_1"},"permissions":{"can_preview":true,"can_download":true,"can_replace":true,"can_delete":true}}],"permissions":{"can_view":true,"can_upload":true},"version":1} |
POST /v0/file/u/ |
Multipart 文件;字段 purpose=meeting_note、meeting_id=mtg_1;Header X-MeetingIM-Auth |
{"ctrl":{"code":200,"params":{"file_id":"file_1","url":"/v0/file/s/file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf"}}} |
POST /v1/im/meetings/{meeting_id}/notes/files 新增 |
Header Idempotency-Key;Body {"file_id":"file_1"} |
{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf","created_at":"2026-09-05T03:00:00Z","version":1,"preview":{"status":"processing","mode":"converted"}} |
PUT /v1/im/meetings/{meeting_id}/notes/files/{note_file_id} 新增 |
Header Idempotency-Key;Body {"file_id":"file_2","version":1} |
{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_2","name":"会议纪要-v2.pdf","version":2,"updated_at":"2026-09-05T03:10:00Z"} |
DELETE /v1/im/meetings/{meeting_id}/notes/files/{note_file_id} 新增 |
Header Idempotency-Key;Query version=2 |
{"note_file_id":"mnote_1","deleted":true,"deleted_at":"2026-09-05T03:20:00Z"} |
GET /v0/file/s/{file_id} |
Header X-MeetingIM-Auth;可带 Range: bytes=0- |
对象存储模式下鉴权通过后返回 302 到短期签名 URL;客户端跟随 Location 获取文件内容 |
9.14.10 字段与状态约定
| 字段 | 取值 | 前端处理 |
|---|---|---|
meeting.state |
scheduled、in_progress、ended、canceled |
会议笔记列表只展示 ended |
note_status |
empty、available |
empty 展示“暂无笔记”,available 展示文件列表 |
preview.status |
ready、processing、unsupported、failed |
分别进入预览、短时刷新、仅下载提示、失败提示 |
preview.mode |
native、converted、download_only |
使用原文件、转换文件或仅提供下载 |
permissions.can_view |
Boolean | 是否可以查看当前会议笔记 |
permissions.can_upload |
Boolean | 是否显示并允许使用上传按钮,仅会议记录员为 true |
permissions.can_replace |
Boolean | 是否允许替换当前笔记文件 |
permissions.can_delete |
Boolean | 是否允许删除当前笔记文件 |
permissions.can_preview |
Boolean | 是否允许预览当前文件 |
permissions.can_download |
Boolean | 是否允许下载当前文件;首版普通参会人若产品要求“只能预览”,该值返回 false |
客户端必须以服务端返回的 permissions 为准。特别是普通参会人“只能预览”的要求,由文件项 can_preview=true,can_download=false 表达;即使预览底层使用文件读取接口,客户端也不得提供导出、系统分享或保存到相册入口。
9.14.11 错误码与客户端处理
reason |
典型 HTTP 状态 | 场景 | 客户端处理 |
|---|---|---|---|
MEETING_NOT_ENDED |
409 | 未结束会议上传或登记笔记 | 提示“会议结束后可上传笔记”,刷新会议详情 |
MEETING_CLOSED |
409 | 会议结束后修改记录员或会议数据 | 切换为只读并刷新详情 |
NOT_MEETING_RECORDER |
403 | 非记录员上传、替换或删除文件 | 隐藏操作入口并刷新权限 |
MEETING_NOTE_VERSION_CONFLICT |
409 | 替换或删除时版本过期 | 拉取最新详情,不自动覆盖 |
MEETING_NOTE_FILE_INVALID |
400 | 文件未上传完成、用途不符或不属于当前租户 | 提示重新上传 |
FILE_PREVIEW_PROCESSING |
409 | 转换预览尚未完成 | 保持当前页并按退避策略刷新 |
FILE_PREVIEW_UNSUPPORTED |
422 | 文件类型不支持在线预览 | 根据 can_download 决定是否提供下载 |
FILE_FORBIDDEN |
403 | 当前用户无文件访问权限 | 关闭预览或停止下载并刷新详情 |
FILE_UNAVAILABLE |
404/410 | 文件被删除、失效或审核拒绝 | 标记不可用并刷新文件列表 |
RESOURCE_NOT_FOUND |
404 | 会议或笔记不可见、跨租户访问 | 返回列表,不提示资源是否真实存在 |
9.14.12 前后端联调与验收要点
- 发起会议时不传
recorder_user_id,响应中的记录员必须默认为当前发起人;指定其他记录员时,响应返回最终记录员和参会人名单。 - 会议笔记列表只包含与当前用户有关的已结束会议;已结束但无文件的会议仍展示,详情为空状态。
- 记录员进入详情显示上传按钮,普通参会人进入同一会议不显示上传按钮;直接调用写接口仍必须被服务端拒绝。
- 普通参会人只允许在线预览时,响应为
can_preview=true,can_download=false,客户端不展示下载、分享或导出入口。 - 文件上传成功但会议笔记登记失败时,详情列表不出现该文件;重复登记同一幂等键返回首次结果。
- 文件替换和删除必须校验
version;旧版本请求返回冲突,不能覆盖新文件。 - 进入列表、进入详情以及从上传/预览/下载页面返回时均主动刷新;断开 WebSocket 不影响会议笔记数据获取。
- 会议笔记新增、替换、删除均不产生 Push、会议通知未读或工作台红点;工作台红点只随会议通知模块变化。
- 跨租户、无关用户和已撤销权限用户无法通过会议 ID 或文件 ID 查看、预览、下载会议笔记。
- 切换企业或退出登录后,会议笔记列表、详情、预览文件和下载临时数据按租户及账号隔离清理。
9.15 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
9.15.1 工作台、组织、打卡、会议、日报、月报、信箱
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/workbench |
Query only | {"layout_version":2,"modules":[{"code":"attendance","name":"打卡","icon_file_id":"","route":"/attendance","sort_order":10,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"monthly_reports","name":"月报","icon_file_id":"","route":"/reports/monthly","sort_order":20,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"daily_reports","name":"日报","icon_file_id":"","route":"/reports/daily","sort_order":30,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"organization","name":"部门框架","icon_file_id":"","route":"/organization","sort_order":40,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"executive_mailbox","name":"举报信箱","icon_file_id":"","route":"/executive-mailbox","sort_order":50,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"meetings","name":"会议通知","icon_file_id":"","route":"/meetings","sort_order":60,"unread_count":3,"enabled":true,"permission":"enabled"},{"code":"meeting_notes","name":"会议笔记","icon_file_id":"","route":"/meeting-notes","sort_order":70,"unread_count":0,"enabled":true,"permission":"enabled"}],"enterprise":{"tenant_code":"default","feature_flags":{"organization":true,"attendance":true,"meetings":true,"meeting_notes":true}}} |
GET /v1/im/organization/departments?parent_id=root&cursor= |
Query only | {"items":[{"department_id":"dep_1","name":"研发部","parent_id":"root","child_count":0,"member_count":12,"version":1}],"next_cursor":""} |
GET /v1/im/organization/departments/{id}/members?cursor= |
Query only | {"items":[{"user_id":"usr...","display_name":"张三","avatar_file_id":"","department_id":"dep_1","title":"工程师","can_contact":true}],"next_cursor":""} |
GET /v1/im/organization/members?query=&department_id=&cursor= 待实现/可由部门成员接口替代 |
Query only | {"items":[{"user_id":"usr...","display_name":"张三","department_id":"dep_1","department_name":"研发部","can_contact":true}],"next_cursor":""} |
GET /v1/im/organization/search?q=&types=department,user&cursor= |
Query only | {"items":[{"type":"user","user_id":"usr...","display_name":"张三","department_name":"研发部","can_contact":true}],"next_cursor":""} |
GET /v1/im/attendance/config?date=2026-08-31 |
Query only | {"date":"2026-08-31","timezone":"Asia/Shanghai","rule":{"rule_id":"att_rule_default","name":"默认考勤规则","workday":true,"location_required":false,"wifi_required":false,"allow_outside_checkin":true,"appeal_allowed":true},"shifts":[{"shift_id":"shift_default","name":"默认班次","checkin_start":"08:30","work_start":"09:00","work_end":"18:00","checkout_end":"20:00"}],"allowed_actions":["check_in","check_out"],"today_record":{"record_id":"evt_...","user_id":"usr...","date":"2026-08-31","status":"checked_in","checkins":[]},"permissions":{"can_checkin":true,"can_appeal":true,"can_view_managed":true}} |
GET /v1/im/attendance/records?scope=mine&from=2026-08-01&to=2026-08-31&cursor= |
Query only | {"items":[{"record_id":"evt_...","user_id":"usr_me","date":"2026-08-31","rule_id":"att_rule_default","shift_id":"shift_default","status":"normal","checkins":[{"checkin_id":"attc_...","action":"check_in","client_time":"2026-08-31T00:55:00Z","server_time":"2026-08-31T00:55:02Z","result":"normal","location":{"name":"上海办公室","distance_meters":38},"wifi":{"ssid":"Office-WiFi"},"device":{"platform":"ios"}}],"exceptions":[],"appeal_status":"","version":3}],"next_cursor":"","has_more":false,"statistics":{"expected_count":1,"checked_count":1,"exception_count":0,"not_checked_count":0},"permissions":{"can_checkin":true,"can_view_managed":true,"can_appeal":true}} |
GET /v1/im/attendance/records?scope=managed&department_id=dep_1&date=2026-08-31&status=late&cursor= |
Query only | {"items":[{"record_id":"evt_...","user_id":"usr...","date":"2026-08-31","status":"late","exceptions":["late"],"appeal_status":"pending","version":2}],"next_cursor":"","has_more":false,"statistics":{"expected_count":20,"checked_count":19,"exception_count":2,"not_checked_count":1}} |
POST /v1/im/attendance/checkins |
Header Idempotency-Key;Body {"date":"2026-08-31","action":"check_in","rule_id":"att_rule_default","shift_id":"shift_default","client_time":"2026-08-31T08:55:00+08:00","location":{"latitude":31.2304,"longitude":121.4737,"accuracy_meters":30},"wifi":{"ssid":"Office-WiFi","bssid":"00:11:22:33:44:55"},"device":{"device_id":"ios-device-1","platform":"ios"}} |
{"record":{"record_id":"evt_...","user_id":"usr...","date":"2026-08-31","status":"checked_in","version":2,"checkins":[{"checkin_id":"attc_...","action":"check_in","result":"normal"}]},"checkin":{"checkin_id":"attc_...","action":"check_in","client_time":"2026-08-31T00:55:00Z","server_time":"2026-08-31T00:55:02Z","result":"normal","location":{"latitude":31.2304,"longitude":121.4737},"wifi":{"ssid":"Office-WiFi"},"device":{"platform":"ios"}}} |
POST /v1/im/attendance/appeals |
Header Idempotency-Key;Body {"record_id":"evt_...","appeal_type":"missing_checkin","reason":"外出拜访客户,忘记打卡","target_action":"check_in","target_time":"2026-08-31T09:00:00+08:00","attachment_file_ids":["file_1"]} |
{"appeal_id":"atta_...","record_id":"evt_...","user_id":"usr...","appeal_type":"missing_checkin","reason":"外出拜访客户,忘记打卡","target_action":"check_in","target_time":"2026-08-31T01:00:00Z","attachment_file_ids":["file_1"],"status":"pending","submitted_at":"2026-08-31T03:00:00Z","version":1} |
GET /v1/im/attendance/appeals?scope=mine&status=pending&cursor= |
Query only | {"items":[{"appeal_id":"atta_...","record_id":"evt_...","user_id":"usr...","appeal_type":"missing_checkin","status":"pending","submitted_at":"2026-08-31T03:00:00Z","version":1}],"next_cursor":"","has_more":false} |
GET /v1/im/meetings?cursor= 待补列表 |
Query only | {"items":[{"meeting_id":"mtg_1","title":"周会","start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","my_response":"pending","state":"scheduled"}],"next_cursor":""} |
POST /v1/im/meetings |
{"title":"周会","start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","location":"线上","recorder_user_id":"usr_me","attendee_user_ids":["usr..."],"reminder_minutes":[10]} |
{"meeting_id":"mtg_1","title":"周会","recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","location":"线上","participants":[{"user_id":"usr_me","response":"accepted"},{"user_id":"usr...","response":"pending"}],"permissions":{"can_update":true,"can_cancel":true,"can_remind":true,"can_respond":false},"version":1} |
GET /v1/im/meetings/{meeting_id} |
Path meeting ID | {"meeting_id":"mtg_1","title":"周会","participants":[{"user_id":"usr...","response":"pending"}],"permissions":{"can_respond":true},"version":1} |
PATCH /v1/im/meetings/{id} |
{"title":"新标题","version":1} |
{"meeting_id":"mtg_1","title":"新标题","version":2,"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/meetings/{id}/cancel |
{"version":2,"reason":"时间冲突"} |
{"meeting_id":"mtg_1","state":"cancelled","version":3} |
POST /v1/im/meetings/{id}/reminders |
{} |
{"meeting_id":"mtg_1","items":[{"user_id":"usr...","status":"sent"}]} |
PUT /v1/im/meetings/{meeting_id}/my-response |
{"response":"accepted","version":1} |
{"meeting_id":"mtg_1","my_response":"accepted","version":2,"response_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/meeting-notes?query=&cursor= |
Query only | {"items":[{"meeting_id":"mtg_1","title":"周会","note_status":"available","file_count":1,"permissions":{"can_upload":true,"can_download":true}}],"next_cursor":"","has_more":false} |
GET /v1/im/meetings/{meeting_id}/notes |
Path meeting ID | {"meeting_id":"mtg_1","note_status":"available","files":[{"note_file_id":"mnote_1","file_id":"file_1","file_name":"会议纪要.pdf","mime_type":"application/pdf","permissions":{"can_download":true}}]} |
POST /v0/file/u/ 会议笔记上传并登记 |
Multipart purpose=meeting_note&meeting_id=mtg_1&id=upload_1&file=@note.pdf |
{"ctrl":{"code":200,"params":{"url":"/v0/file/s/file_1.pdf","file_id":"file_1","purpose":"meeting_note","meeting_id":"mtg_1","note_file_id":"mnote_1","note_file_status":"active","version":1}}} |
POST /v1/im/meetings/{meeting_id}/notes/files |
Header Idempotency-Key;Body {"file_id":"file_1"} |
{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_1","status":"active","version":1} |
GET /v1/im/reports/daily?scope=mine&from=&to=&status=&cursor= |
Query only | {"items":[{"report_id":"dr_1","report_date":"2026-08-20","content_short":"今日完成联调和问题回归...","status":"draft","draft_updated_at":"2026-08-20T09:00:00Z","submitted_at":null,"read_state":"unread"}],"next_cursor":"","permissions":{"can_create":true,"makeup_allowed_days":7}} |
GET /v1/im/reports/daily?scope=managed&date=&status=submitted|missing&department_id=&cursor= |
Query only | {"items":[{"report_id":"dr_1","author":{"user_id":"usr...","display_name":"张三"},"report_date":"2026-08-20","content_short":"完成日报提交和项目同步...","status":"submitted","submitted_at":"2026-08-20T09:00:00Z","read_state":"unread"}],"next_cursor":"","permissions":{"can_remind_unsubmitted":true,"can_view_managed":true}} |
POST /v1/im/reports/daily |
{"report_date":"2026-08-20"} |
{"report_id":"dr_1","report_date":"2026-08-20","content":"","attachments":[],"status":"draft","version":1,"permissions":{"can_edit":true,"can_submit":true,"can_view":true}} |
PUT /v1/im/reports/daily/{id}/draft |
{"content":"今日完成联调","attachments":[],"version":1} |
{"report_id":"dr_1","content":"今日完成联调","attachments":[],"status":"draft","version":2,"draft_updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/reports/daily/{id}/submit |
{"version":2} |
{"report_id":"dr_1","status":"submitted","submitted_at":"2026-08-20T09:00:00Z","version":3} |
GET /v1/im/reports/daily/statistics?date=&department_id= |
Query only | {"expected_count":10,"submitted_count":8,"missing_count":2,"scope":{"department_id":"dep_1"}} |
POST /v1/im/reports/daily/reminders |
{"date":"2026-08-20","user_ids":["usr..."]} |
{"date":"2026-08-20","items":[{"user_id":"usr...","status":"sent"}]} |
GET /v1/im/reports/daily/{report_id} |
Path report ID | {"report_id":"dr_1","author":{"user_id":"usr...","display_name":"张三"},"report_date":"2026-08-20","content":"日报内容","attachments":[],"status":"submitted","version":3,"permissions":{"can_view":true,"can_edit":false}} |
GET /v1/im/reports/monthly?scope=mine&from_month=&to_month=&status=&cursor= |
Query only | {"items":[{"report_id":"mr_1","month":"2026-08","author_id":"usr...","work_summary_short":"本月完成客户拜访和合同整理...","next_plan_short":"下月推进回款和方案评审","status":"draft","action":"edit","updated_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
GET /v1/im/reports/monthly?scope=mine|managed&from_month=&to_month=&status=&cursor= |
Query only | {"items":[{"report_id":"mr_1","month":"2026-08","author_id":"usr...","work_summary_short":"完成项目阶段验收...","next_plan_short":"继续推进上线准备","status":"submitted","action":"view","submitted_at":"2026-08-20T09:00:00Z"}],"next_cursor":""} |
POST /v1/im/reports/monthly |
{"month":"2026-08"} |
{"report_id":"mr_1","month":"2026-08","work_summary":"","next_plan":"","attachments":[],"status":"draft","action":"edit","version":1,"validation":{"work_summary_required":true,"next_plan_required":true,"max_length":10000},"permissions":{"can_edit":true,"can_submit":true}} |
PUT /v1/im/reports/monthly/{id}/draft |
{"work_summary":"本月总结","next_plan":"下月计划","attachments":[],"version":1} |
{"report_id":"mr_1","work_summary":"本月总结","next_plan":"下月计划","status":"draft","version":2} |
POST /v1/im/reports/monthly/{id}/submit |
{"version":2} |
{"report_id":"mr_1","status":"submitted","submitted_at":"2026-08-20T09:00:00Z","version":3} |
GET /v1/im/reports/monthly/statistics?month=&department_id= |
Query only | {"expected_count":10,"submitted_count":8,"missing_count":2,"scope":{"department_id":"dep_1"}} |
GET /v1/im/reports/monthly/{report_id} |
Path report ID | {"report_id":"mr_1","month":"2026-08","author":{"user_id":"usr...","display_name":"张三"},"work_summary":"总结","next_plan":"计划","attachments":[],"status":"submitted","version":3,"permissions":{"can_view":true,"can_edit":false}} |
POST /v1/im/reports/monthly/{id}/reopen 待实现 |
{"version":3,"reason":"补充内容"} |
{"report_id":"mr_1","status":"draft","version":4} |
GET /v1/im/executive-mailbox/config |
Query only | {"enabled":true,"allow_anonymous":true,"required_fields":["subject","content"],"max_length":1000,"attachment_policy":{"max_files":5,"purpose":"executive_mailbox"},"notice":{"title":"总经理信箱","content":"请如实反馈","updated_at":"2026-08-20T09:00:00Z"}} |
POST /v1/im/executive-mailbox/submissions |
Header Idempotency-Key;Body {"subject":"建议","content":"建议内容","anonymous":false,"attachment_file_ids":[]} |
{"submission_id":"mail_1","status":"received","submitted_at":"2026-08-20T09:00:00Z"} |
10. 个人资料、通用设置、隐私与青少年模式
10.1 我的.png
状态:基础用户资料已有;产品菜单新增。
GET /v1/im/me返回公开账号资料、实名/青少年模式摘要和资料版本;GET /v1/im/bootstrap返回个人菜单与权限。- 头像、昵称更新事件到达后刷新头部;收藏和设置入口分别进入第 7、10 章。
- 账号 ID 按字符串展示,客户端不得展示内部数据库 ID 或 tenant_id。
10.2 个人信息.png
状态:MeetingIM me 资料更新已有基础;目标产品接口需改造。
- 查询
GET /v1/im/me/profile返回username,avatar_file_id,nickname,birth_date,signature,gender,region,email,public_account,phone_mask,version,field_rules;username为只读登录账号;phone_mask为只读脱敏手机号(例如136****3333),未绑定手机号时返回空字符串;email当前仅为资料字段,不用于登录或找回密码。 - 更新统一使用
PATCH /v1/im/me/profile,仅提交变更字段并带version;服务端更新后同步 MeetingIMme.public并推送资料事件。 - 头像、昵称、签名、性别、地区、公开账号等资料字段以
im_user_profiles为业务真源;im_users.public和im_subscriptions.public只作为服务端生成的展示投影/缓存,前端不得依赖它们作为可编辑源。 - 头像先以
purpose=user_avatar上传再 PATCH;任何字段的可见性由隐私策略裁剪。
10.3 设置昵称.png
状态:需改造。
PATCH /v1/im/me/profile,Body{"nickname":"新昵称","version":n}。- 服务端校验 Unicode 长度、全空白、控制字符和敏感内容;返回更新后的
nickname,version,updated_at。 - 成功刷新全局资料和联系人快照,但不得覆盖各群昵称。版本冲突先拉最新资料再由用户确认。
10.4 出生日期.png
状态:新增产品字段。
- 保存同 10.3,Body
{"birth_date":"2000-01-02","version":n};仅传 ISO 日期,不传本地化文本或时区时间戳。 - 服务端校验不晚于当前租户日期及合理范围,返回
PROFILE_FIELD_INVALID。展示格式由客户端本地化。
10.5 个性签名.png
状态:需改造。
- 保存同 10.3,Body
{"signature":"...","version":n};空字符串表示清空。 - 服务端按
field_rules.signature.max_length和内容审核处理;失败保留本地草稿,成功刷新引用页面。
10.6 二维码.png
状态:图标纯客户端;二维码协议新增。
- 点击入口进入 5.4 并创建短期个人二维码。每次主动刷新调用
POST /v1/im/me/qrcodes获取新 Token。 - 二维码不能使用静态
user_id、手机号或登录 Token;过期、撤销和切租户后必须重新生成。
10.7 设置(iOS).png
状态:菜单纯客户端;数据接口部分新增。
- 菜单来自
bootstrap.settings_menu,安全、通知、隐私、通用、关于按权限展示。系统通知权限状态由 iOS API 读取,不由服务端伪造。 - 退出:
POST /v1/im/auth/logout将当前登录 Token 的 SHA-256 摘要写入 Redis 撤销集合,TTL 与 Token 剩余有效期一致,不影响同账号其他设备。成功后关闭 WS、清安全存储和本租户用户缓存;请求须带Authorization: Bearer <token>和Idempotency-Key。 - 多设备退出使用
DELETE /v1/im/security/sessions/{session_id},需二次验证。
10.8 设置(安卓).png
状态:菜单纯客户端;业务协议同 10.7。
- 使用相同
bootstrap.settings_menu与退出协议;Android 系统权限、通知渠道和应用更新使用平台实现。 - 服务端只保存应用内通知偏好与设备 Push Token,不把系统设置页面是否打开当作已授权。
10.9 清空缓存的提示.png
状态:纯客户端。
- 确认后只删除缩略图、可重下文件、临时上传下载和搜索缓存;不调用服务端删除消息、收藏或账号接口。
- 不删除安全存储中的登录凭证,除非用户选择退出登录。完成后重新计算本地占用并显示结果。
10.10 版本更新页面.png
状态:新增 App 配置接口。
完整接口与 Android OSS APK 对接流程见:IM-App版本更新前后端对接文档。
GET /v1/im/app/releases/latest?platform=ios|android|web¤t_version=1.0.0&channel=stable,全局公共接口,不依赖租户解析。current_version必传,使用数字点分版本号(如1.2.0)。- 服务端返回
has_update、force_update和update_type=none|recommended|force;iOS 客户端使用store_url跳转 App Store。 - Android 客户端使用
package_url下载 OSS/CDN 上的 APK,并在安装前使用checksum完成 SHA-256 校验;强制更新时不得绕过进入主业务。 - 版本策略由 Admin 在
hotgo.im_app_releases统一管理,IM Server 通过 HMAC 内部配置接口读取,不从租户库查询。 - 返回
current_version,has_update,force_update,latest_version,min_supported_version,update_type=none|recommended|force,release_notes,store_url,package_url,checksum,published_at。 - 客户端按平台只打开受信任商店/签名下载地址;Android 包必须校验签名和 checksum,服务端 URL 使用允许域名。
10.11 发现新版本.png
状态:纯客户端弹窗;数据来自 10.10。
optional显示稍后/更新,记录本地已忽略版本并按服务端remind_after再提示;required不显示绕过入口。- 下载和安装状态由平台管理;接口异常时不得误判为强制更新,除非本地已有有效签名的最低版本策略缓存。
10.12 隐私设置.png
状态:新增。
GET /v1/im/me/privacy返回add_me_methods,profile_visibility,last_seen_visibility,read_receipts,friend_request_verification,group_invite_verification,version和各字段editable;friend_request_verification=true表示别人加我为好友时需要我确认,false时符合来源规则的好友申请自动通过;group_invite_verification=true表示别人邀请我入群时需要走入群审批。PATCH /v1/im/me/privacy只提交变更字段和version;成功返回完整最新配置并向其他设备推送privacy.updated。- 黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。
10.13 隐私设置-添加我的方式选项.png
状态:新增。
add_me_methods固定字段account,phone,qr,group,每项为布尔值;更新用 10.12 PATCH,如{"add_me_methods":{"phone":false},"version":n}。- 关闭后对应搜索/二维码/群来源申请由服务端统一返回
FRIEND_REQUEST_NOT_ALLOWED或USER_NOT_DISCOVERABLE,不泄露具体关闭项。 - 企业强制策略可令字段
editable=false,前端禁用并显示服务端提供的通用说明。
10.14 青少年模式未开启(安卓).png
状态:新增。
GET /v1/im/settings/youth-mode返回enabled,policy_version,restrictions,restriction_items,password_set,can_enable。当前默认策略项为night_quiet、content_filter、payment_guard三条;后端现阶段完成状态持久化、开启/关闭校验和策略返回;night_quiet已接入音视频呼叫发起限制,content_filter已接入消息发送前内容安全审核,二者返回enforced=true;payment_guard仍为预留项,返回enforced=false。- 点击开启先展示 10.15,再按 1.7 完成身份验证和 1.11 设置独立密码;最后调用
POST /v1/im/settings/youth-mode/enable,Body{"policy_version":n,"action_token":"..."},其中action_token使用设置独立密码响应里的enable_action_token。 - 开启链路中,短信校验返回的
set_independent_passwordtoken 只用于设置独立密码;设置密码成功响应中的enable_action_token才用于youth-mode/enable。 - 服务端持久化到用户租户范围并推送所有设备,不能仅设置本地标志。
10.15 青少年模式提示(安卓).png
状态:弹窗纯客户端;文案由服务端配置。
- 使用 10.14 返回的
restriction_items与policy_version渲染;取消不请求,确认进入密码设置/验证。restrictions仅作为兼容旧客户端的 code 数组。 - 最终 enable 请求携带用户确认的
policy_version;若策略更新返回POLICY_RECONFIRM_REQUIRED,重新展示新内容。
10.16 青少年模式已开启(安卓).png
状态:新增。
- 页面调用 10.14,
enabled=true时展示enabled_at,restrictions。bootstrap同时返回受限模块和动作。 - 受限接口服务端返回
YOUTH_MODE_RESTRICTED,当前已覆盖夜间音视频呼叫发起;前端隐藏只是体验优化,不能作为安全控制。 - 登录、App 重启和另一设备均从服务端恢复状态;收到
youth_mode.updated即刷新。
10.17 关闭青少年模式(安卓).png
状态:新增。
- 先调用 1.11 的独立密码验证,获得
action_token;再POST /v1/im/settings/youth-mode/disable,HeaderX-Action-Token,带幂等键。 - 成功返回
enabled:false,updated_at并广播;连续密码错误按服务端retry_after_seconds锁定。 - 忘记密码走 1.7 身份验证后重置,不能绕过密码直接修改本地状态。
10.18 获取推送权限(安卓).png
状态:权限引导纯客户端;设备注册需改造。
- 用户确认后调用 Android 系统权限;拒绝状态本地记录,最终以系统回调为准。
- 获取 Push Token 后
PUT /v1/im/devices/{device_id}/push-token,Body{"provider":"fcm","token":"...","app_version":"..."};Token 轮换时覆盖。 - 退出登录删除/解绑该用户设备 Token。Push Token 属敏感凭证,不写日志。
10.19 提醒(安卓).png
状态:纯客户端。
- 仅在功能因系统权限不可用时显示;取消和“前往设置”不调用业务服务端。
- 回到应用后重新读取真实权限,不能因用户打开过设置页就假定授权成功。
10.20 相机说明(安卓).png
状态:纯客户端。
- 首次拍照、扫码或视频通话前展示用途说明,确认后再触发系统权限;拒绝不发媒体/扫码请求。
- 永久拒绝时提供系统设置入口;允许从相册选择等替代能力。客户端不得上传系统权限状态作为服务端授权依据。
10.21 麦克风说明(安卓).png
状态:纯客户端。
- 首次录音或通话前说明并申请系统权限;未授权时不得创建录音文件或发送 call invite。
- 永久拒绝后进入系统设置引导;恢复授权后重新执行 4.7 或第 8 章动作,不复用过期媒体会话。
10.22 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
10.22.1 个人资料、设置、隐私、青少年模式与设备
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
GET /v1/im/me |
Query only | {"user_id":"usr...","profile":{"public_account":"usr...","nickname":"张三","avatar_file_id":"","signature":"","version":1},"profile_version":1,"realname":{"status":"unverified"},"youth_mode":{"enabled":false}} |
GET /v1/im/me/profile |
Query only | {"username":"zhangsan7","avatar_file_id":"","nickname":"张三","birth_date":"1990-01-01","signature":"签名","gender":"male","region":"上海","email":"a@example.com","public_account":"usr...","phone_mask":"136****3333","version":1,"field_rules":{"nickname":{"min_length":1,"max_length":32},"signature":{"max_length":120}}} |
PATCH /v1/im/me/profile |
{"nickname":"新昵称","signature":"新签名","version":1} |
{"avatar_file_id":"","nickname":"新昵称","signature":"新签名","version":2,"updated_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/me/privacy |
Query only | {"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"friend_request_verification":true,"group_invite_verification":false,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true,"friend_request_verification":true,"group_invite_verification":true}} |
PATCH /v1/im/me/privacy |
{"add_me_methods":{"phone":false},"read_receipts":false,"friend_request_verification":false,"group_invite_verification":true,"version":1} |
{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"friend_request_verification":false,"group_invite_verification":true,"version":2,"updated_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/settings/youth-mode |
Query only | {"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":true},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":true},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/settings/independent-password |
{"password":"1234","purpose":"youth_mode","action_token":"action_token"} |
{"status":"ok","password_set":true,"purpose":"youth_mode","enable_action_token":"enable_token","enable_action_expires_at":"2026-08-20T09:05:00Z","next_action":"enable_youth_mode"} |
POST /v1/im/settings/independent-password/verify |
{"password":"1234","purpose":"close_youth_mode"} |
{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"} |
POST /v1/im/settings/youth-mode/enable |
{"policy_version":1,"action_token":"enable_action_token"} |
{"enabled":true,"policy_version":1,"password_set":true,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":true},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":true},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/settings/youth-mode/disable |
Header X-Action-Token: action_token;Body {} |
{"enabled":false,"policy_version":1,"password_set":true,"updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/security/challenges 开启青少年模式 |
Header Idempotency-Key;Body {"scene":"set_independent_password","channel":"sms"} |
{"challenge_id":"sec_chg_1","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60} |
POST /v1/im/security/challenges 关闭青少年模式 |
Header Idempotency-Key;Body {"scene":"close_youth_mode","channel":"sms"} |
{"challenge_id":"sec_chg_2","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60} |
POST /v1/im/security/challenges 限流 |
同上 | HTTP 429:{"code":30020,"reason":"VERIFICATION_RATE_LIMITED","message":"verification rate limited","details":{"challenge_id":"sec_chg_1","retry_after_seconds":45}};小时限流时 retry_after_seconds 可能为数百/数千秒且可不返回 challenge_id |
POST /v1/im/security/challenges/{id}/verify 开启青少年模式 |
{"code":"123456"} |
{"action_token":"set_password_token","action":"set_independent_password","expires_at":"2026-08-20T09:05:00Z"} |
POST /v1/im/security/challenges/{id}/verify 关闭青少年模式 |
{"code":"123456"} |
{"action_token":"close_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"} |
GET /v1/im/security/realname |
Query only | {"status":"unverified","can_submit":true} |
POST /v1/im/security/realname/submissions |
{"name":"张三","id_type":"cn_id","id_number":"110101199001011234"} |
{"submission_id":"rn_1","status":"pending","can_submit":false} |
POST /v1/im/security/realname/appeals |
{"reason":"证件变更需要更正实名信息","attachments":["file_1"]} |
{"appeal_id":"rna_1","reason":"...","attachments":["file_1"],"status":"pending"};当前无需二次身份验证(如后续启用,将复用 1.7 的 action_token 机制并在此处更新) |
PUT /v1/im/devices/{device_id}/push-token |
{"provider":"jpush","token":"registration_id","platform":"android","app_version":"1.0.0","push_env":"test","sdk_version":"5.0.0","notification_permission":"granted"} |
{"device_id":"device_1","provider":"jpush","platform":"android","app_version":"1.0.0","updated_at":"2026-08-20T09:00:00Z"} |
DELETE /v1/im/security/sessions/{session_id} 待实现 |
Header X-Action-Token: action_token |
{"session_id":"sess_1","revoked":true} |
POST /v1/im/me/password |
{"old_password":"OldPass123","new_password":"NewPass123"} |
{"status":"ok"};旧密码错误返回 HTTP 401:{"code":30001,"reason":"AUTH_REQUIRED","message":"invalid credentials"};新密码不满足策略返回 HTTP 400:{"code":30040,"reason":"PASSWORD_POLICY_VIOLATION"} |
POST /v1/im/security/challenges 注销账号发码 |
Header Idempotency-Key;Body {"scene":"delete_account","channel":"sms"} |
{"challenge_id":"sec_chg_9","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60} |
POST /v1/im/security/challenges/{id}/verify 注销账号验证 |
{"code":"123456"} |
{"action_token":"del_token","action":"delete_account","expires_at":"2026-08-20T09:05:00Z"} |
POST /v1/im/me/deletion |
Header X-Action-Token: del_token;Body {} |
{"status":"ok"} |
10.22.2 账号安全:修改密码与注销账号
修改密码 POST /v1/im/me/password(已实现):
- 已登录(Bearer)用户提交
old_password、new_password;两者必填,缺一返回400 MALFORMED_REQUEST。 - 服务端先用旧密码走
basic认证校验,失败返回 HTTP401:{"code":30001,"reason":"AUTH_REQUIRED","message":"invalid credentials"},不泄露账号状态。 - 新密码不满足密码策略返回 HTTP
400:{"code":30040,"reason":"PASSWORD_POLICY_VIOLATION"}。 - 成功后服务端递增
auth_version并撤销该用户全部在线 Session(广播password_changed);包括当前请求在内的所有旧 Token 全部失效,客户端收到成功响应后必须清理本租户凭证并跳转登录页。
注销账号 POST /v1/im/me/deletion(已实现):
- 两步挑战复用 1.7 敏感操作基建:
POST /v1/im/security/challenges(Body{"scene":"delete_account","channel":"sms"},发码到账号绑定手机号)→POST /v1/im/security/challenges/{id}/verify(返回一次性action_token)→POST /v1/im/me/deletion(HeaderX-Action-Token携带)。 - 服务端配置
permanent_accounts=true时拒绝注销:HTTP403{"code":30040,"reason":"FORBIDDEN","message":"account deletion is disabled"}。 - 执行成功为软删除:撤销全部认证记录(含登录密码)、踢出所有在线 Session(
reason=user_deleted)、退出并停用其拥有的群与 P2P 会话、向关注该用户的好友下发gone在线状态;账号不再可登录。客户端收到成功响应后清理本地数据并返回企业入口。 - 挑战阶段限流与错误码同 1.7(
VERIFICATION_RATE_LIMITED、CHALLENGE_EXPIRED、CODE_INVALID、TOO_MANY_ATTEMPTS);action token 缺失/无效返回 HTTP403{"code":30042,"reason":"ACTION_TOKEN_INVALID"}。
11. 反诈安全、举报与意见反馈
11.1 反诈弹窗(安卓).png
状态:新增风控决策协议。
- 高风险动作在原业务接口返回
409 RISK_CONFIRMATION_REQUIRED,details={risk_challenge_id,level,notice_code,notice_params,expires_at,action}。 - 客户端用
notice_code映射经过审核的本地文案,不显示服务端规则、分数或命中特征;取消不重试原操作。 - 低风险提示也可由服务端可信安全事件触发。当前风险确认/取消会按 0.5.4.6 下发
security.risk.continue/cancel;普通消息不得伪装成系统反诈弹窗。
11.2 反诈弹窗(二次提示安卓).png
状态:新增。
- 确认:
POST /v1/im/security/risk-challenges/{risk_challenge_id}/confirm,Body{"decision":"continue|cancel"},带幂等键。 continue成功返回单次、短期risk_action_token;客户端在重试原业务接口时以X-Risk-Action-Token携带,原请求幂等键保持不变。- 服务端可返回
RISK_ACTION_BLOCKED直接阻断;确认、取消、过期均写审计,客户端不得无限创建 challenge 绕过。
11.3 举报.png
状态:新增。
- 原因:
GET /v1/im/reports/abuse/reasons?target_type=user|group|message返回reason_code,label,requires_description,allows_evidence。 - 进入页面携带受控目标:用户
user_id、群topic或消息(topic,seq);客户端不提交 tenant_id 或被举报者任意文本身份。 - 服务端先验证举报人能够看到目标,再返回适用原因;举报人身份不下发给被举报方。
11.4 举报(1).png
状态:新增。
- 证据使用
purpose=abuse_evidence上传;提交POST /v1/im/reports/abuse,Body{"target":{"type":"message","topic":"...","seq":"12"},"reason_code":"fraud","description":"...","evidence_file_ids":[]}。 - 服务端从目标消息保存必要、受控的审核快照,校验证据数量/MIME/大小,返回
report_id,status:"received"。 - 重复举报按策略合并并返回原受理号或
REPORT_RATE_LIMITED;证据使用独立严格 ACL 和保留期。
11.5 意见反馈.png
状态:新增。
- 配置:
GET /v1/im/feedback/config返回类别、最大正文、附件规则、是否允许联系方式和日志授权说明。 - 提交:
POST /v1/im/feedback,Body{"category":"bug","content":"...","contact":"...","attachment_file_ids":[],"client_context":{"platform":"android","app_version":"1.0.0","os_version":"..."},"include_diagnostics":false}。 - 附件用
purpose=feedback;只有用户明确同意include_diagnostics=true才上传经过脱敏的诊断包。成功返回feedback_id,status:"received",重复提交由幂等键去重。
11.6 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
- 产品 HTTP 均使用
https://{host}/{api_path}v1/im,请求头包含X-MeetingIM-APIKey、X-IM-Schema: 1、X-Request-ID;登录后接口再带Authorization: Bearer <token>。 - 写接口必须带
Idempotency-Key。二次认证接口按正文说明额外带X-Action-Token或X-Risk-Action-Token。 - 成功响应统一包裹为
{"code":0,"data":示例,"request_id":"...","server_time":"..."};下表只展示data。 - 失败响应统一包裹为
{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}。 - 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
11.6.1 反诈、举报与意见反馈
| 接口 | 输入示例 | 成功 data 示例 |
|---|---|---|
POST /v1/im/security/risk-challenges/{risk_challenge_id}/confirm |
{"decision":"continue"} |
{"risk_action_token":"risk_token","action":"send_message","expires_at":"2026-08-20T09:05:00Z"} |
GET /v1/im/reports/abuse/reasons?target_type=user|group|message 需修复 |
Query only | {"items":[{"reason_code":"fraud","label":"诈骗","requires_description":true,"allows_evidence":true},{"reason_code":"harassment","label":"骚扰","requires_description":false,"allows_evidence":true}]} |
POST /v1/im/reports/abuse |
{"target":{"type":"message","topic":"grp...","seq":"12"},"reason_code":"fraud","description":"疑似诈骗","evidence_file_ids":["file_1"]} |
{"report_id":"abr_1","status":"received","submitted_at":"2026-08-20T09:00:00Z"} |
GET /v1/im/feedback/config |
Query only | {"categories":["bug","suggestion","complaint","other"],"max_length":1000,"allow_contact":true,"attachment_policy":{"max_files":5,"purpose":"feedback"},"diagnostics_notice":"仅在你同意时上传脱敏诊断信息。","updated_at":"2026-08-20T09:00:00Z"} |
POST /v1/im/feedback |
{"category":"bug","content":"页面异常","contact":"15500000000","attachment_file_ids":[],"client_context":{"platform":"android","app_version":"1.0.0","os_version":"14"},"include_diagnostics":false} |
{"feedback_id":"fb_1","status":"received","created_at":"2026-08-20T09:00:00Z"} |
12. 联调实施清单
12.1 当前可直接联调的基础能力
- WebSocket
/v0/channels、Wire Version0.25和最低版本0.20。 - 第一条
hi携带企业码并固定 Session 租户;响应包含公开租户信息、限制、ICE 配置和呼叫超时。 basic/token登录,sub/get/set/pub/leave/del/note基础 MeetingIM 协议。pub成功返回202 + seq,Topic 消息与基础回执同步。/v0/file/u/和/v0/file/s/{file_id}的基础上传下载及租户存储隔离。
12.2 必须优先完成的服务端阻塞项
| 优先级 | 改造项 | 阻塞图片章节 |
|---|---|---|
| P0 | /v1/im 统一认证中间件、响应封装、幂等键、租户 Principal |
全部 HTTP 产品能力 |
| P0 | 企业码解析、验证码、密码重置、二次认证、Token auth_version、登录换新和撤销 |
1.1~1.11、10.7~10.8 |
| P0 | 普通用户禁止发布系统 Topic;系统通知和业务事件可信生产通道 | 2.5、3.1~3.5、6.4、11.1 |
| P0 | 消息 x-im-schema/type/client-mid 校验和服务端幂等 |
2~4 章 |
| P0 | 文件资源级 ACL、Range、ETag、用途绑定和租户存储配额 | 3.6、4.10~4.14、6.27~7.16、9.14、11.4~11.5 |
| P1 | 好友申请、好友关系、黑名单状态机 | 第 3、5 章 |
| P1 | 群成员、角色、禁言、公告、邀请审批和配额 | 第 6 章 |
| P1 | 收藏、转发和聊天记录搜索 | 第 4、7 章 |
| P1 | 通话媒体状态、结果消息、Push 来电 | 第 8 章 |
| P2 | 工作台、组织、会议、会议笔记、日报、月报、信箱 | 第 9 章 |
| P2 | 隐私、青少年模式、反诈、举报、反馈 | 第 10、11 章 |
12.3 前后端联调最低验收用例
| 编号 | 场景 | 预期结果 |
|---|---|---|
| T-01 | A 企业 Token 连接 B 企业 Session | 登录失败 TENANT_MISMATCH,不产生用户/Topic 数据 |
| T-02 | 企业停用后使用旧 Token 登录和请求 HTTP | WS/HTTP 均拒绝,客户端清除凭证并回企业入口 |
| T-03 | 同 client_mid 断线重发同一消息 |
只落一条消息并返回同一 seq |
| T-04 | 相同 HTTP 幂等键、相同请求体重复提交 | 返回第一次结果;不同请求体返回 IDEMPOTENCY_CONFLICT |
| T-05 | 被拉黑或删除好友后绕过按钮直接 pub |
服务端拒绝,对方不收到消息 |
| T-06 | 普通用户构造 system_event 或向系统 Topic 发布 |
服务端拒绝,系统消息页不展示可信样式 |
| T-07 | 普通群成员调用管理员、禁言、转让群主接口 | 返回 FORBIDDEN,群状态不变且写安全审计 |
| T-08 | 群成员数已达租户配置上限时同意两个并发申请 | 至多一个成功,成员数不超限 |
| T-09 | 非消息接收者持同租户 Token 下载文件 | 返回 FILE_FORBIDDEN,不能只通过 file_id 下载 |
| T-10 | 文件下载中断后以有效 Token 登录取得新 Token 并 Range 续传 | 文件完整、checksum 正确、不重复下载已完成分片 |
| T-11 | 风控二次确认后重复重放 risk token | 只有首次有效,其余返回 Token 已消费 |
| T-12 | 切换企业后查看缓存、上传任务、收藏和搜索 | 不出现上一租户数据或任务 |
12.4 统一错误 reason
首版客户端至少实现:TENANT_UNAVAILABLE、TENANT_MISMATCH、AUTH_FAILED、TOKEN_EXPIRED、ACCOUNT_DISABLED、RATE_LIMITED、VALIDATION_FAILED、IDEMPOTENCY_CONFLICT、RESOURCE_NOT_FOUND、FORBIDDEN、FRIEND_RELATION_REQUIRED、MESSAGE_NOT_ALLOWED、GROUP_MUTED、GROUP_MEMBER_LIMIT_EXCEEDED、TENANT_USER_QUOTA_EXCEEDED、TENANT_GROUP_QUOTA_EXCEEDED、FILE_TOO_LARGE、FILE_FORBIDDEN、FILE_UNAVAILABLE、STORAGE_QUOTA_EXCEEDED、REALNAME_REQUIRED、YOUTH_MODE_RESTRICTED、RISK_CONFIRMATION_REQUIRED、RISK_ACTION_BLOCKED。
未知 reason 使用通用失败文案并记录 request_id,不得把服务端 message 原样展示给用户。
12.5 完成定义
每个图片小节进入联调前,服务端须提供 OpenAPI/JSON Schema、错误码、权限规则和可用测试租户;前端须提供请求日志脱敏、断线/重试行为和四态页面(加载、空、成功、失败)。联调完成必须逐图勾选对应协议、至少覆盖成功、权限拒绝、重复提交、断网恢复和跨租户五类场景。