版本:v2.0
日期:2026-08-11
需求依据:IM 产品原型需求文档
设计依据:IM 系统设计文档
说明:第 1 至第 11 章的章节编号、图片标题和顺序与产品原型完全一致。每张图均给出可直接开发、联调和验收的协议。
| 状态 | 含义 |
|---|---|
已实现 | 当前服务端已有能力,可按本文联调 |
需改造 | 已有基础能力,但字段、鉴权或业务规则需要调整 |
新增 | 当前服务端没有该产品接口或数据模型 |
纯客户端 | 界面或系统能力,不调用服务端;关联的提交协议会单独指出 |
文档中所有 新增、需改造 接口都是目标契约,不代表当前代码已经可用。前后端必须以本文字段为准实施,接口完成前可使用同结构 Mock。
API Key 不是客户端启动后向 IM 服务端申请得到的值。现有代码没有“获取 API Key”的 HTTP 或 WebSocket 接口。
API Key 是应用级接入标识,由部署人员在发布客户端前生成并配置:
go run ./keygen。api_key_salt 时,执行 go run ./keygen -sequence <新序号> -salt '<服务端 api_key_salt>' 生成兼容的普通应用 Key。HMAC salt 配置到服务端 server/tinode.conf 的 api_key_salt;输出中的 API key 配置到 iOS、Android 或 Web 客户端构建配置/受控环境配置。-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 | 应用发布/环境配置时预置 | 允许应用访问 Tinode 入口 | 否 |
tenant_code | 用户输入企业码 | 将 Session 路由并绑定到企业 | 否 |
token | 用户登录成功后由服务端返回 | 标识当前租户内的登录用户 | 是 |
API Key 可以被客户端应用提取,因此不能把它当成用户密码或租户授权。现有 checkAPIKey 只校验 Key 的格式和 HMAC 签名;最终权限仍必须依赖租户 Session、用户 Token 和 Topic/业务 ACL。
当前 checkAPIKey 没有按 sequence 吊销旧 Key 的逻辑。生成新序号不会自动让旧 Key 失效;若需要精细轮换,服务端必须增加最低可接受序号或撤销表。直接更换 api_key_salt 会让所有旧 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-Tinode-APIKey | 同上 |
| 产品 HTTP | https://{host}/{api_path}v1/im/* | Authorization: Bearer <params.token> | 好友、群、收藏、搜索、工作台和设置 |
| 文件 HTTP | POST /v0/file/u/、GET /v0/file/s/{file_id} | 当前用 X-Tinode-Auth: token <base64_token> | 文件上传下载;禁止 Query 传 Token |
HTTP 请求统一通过 X-Tinode-APIKey 携带。浏览器 WebSocket 无法设置自定义 Upgrade Header 时才使用 apikey Query;日志和监控必须对该参数脱敏。产品 HTTP 还必须带 X-IM-Schema: 1 和 X-Request-ID;写操作带 Idempotency-Key。生产环境只允许 HTTPS/WSS。Token、密码、验证码、证件号不得写入 URL、普通日志、埋点或崩溃报告。
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 与租户绑定关系,不能只修改客户端配置。
tenant_code;客户端永远不接收、不提交数据库 tenant_id。{hi},其中 tenant 为企业码。服务端解析后把租户固定到 Session。TENANT_MISMATCH,不得自动切换。tenant_id、user_id 不能覆盖身份。RESOURCE_NOT_FOUND 或 FORBIDDEN,不得暴露对象是否存在。成功响应统一为:
{
"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。
读取客户端预置的 host 和普通 API Key
-> 用户输入/客户端读取 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,用于进入企业前校验企业码并返回公开配置;但它不能代替 WebSocket 第一帧 {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)。Token 登录必须把上次 params.token 的 JSON 字符串原样填入 secret,不能再次 Base64 编码。登录成功 {ctrl} 的 params 至少包含 user、authlvl、token、expires。产品 HTTP 也使用同一个 params.token:Authorization: Bearer <params.token>,客户端不要解码或二次编码。Token 对客户端是不可解析的 opaque 字符串,只能存入系统安全存储。id 只用于匹配本次 Session 的命令响应,不是业务幂等键。
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}}}}
订阅成功后才允许 {pub}、note read/recv 等会话操作。断线恢复必须重新执行 WebSocket Upgrade -> hi -> token login -> sub me -> 恢复 Topic,同一连接内不得跳过 hi 或切换企业码。
如果客户端无法使用 WebSocket,可以使用现有 /v0/channels/lp。该模式确实需要“先请求取得一个值”,但取得的是短期 Session ID sid,API Key 仍然必须由客户端预先持有。
第一步,创建 Long Polling Session:
POST /v0/channels/lp HTTP/1.1
Host: im.example.com
X-Tinode-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-Tinode-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-Tinode-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。
第 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 不符合当前发布环境,客户端应优先使用内置的公网环境配置,并上报脱敏诊断。
核心命令: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 显示“暂不支持的消息”,不得崩溃或删除消息。
消息类型首版固定为:text、image、video、voice、file、contact_card、link、forward_bundle、call、system_event。自定义表情启用后追加 sticker。媒体内容只保存 file_id、文件元数据和缩略图 ID,不保存可长期绕过鉴权的公共 URL。
Idempotency-Key。retry_after_seconds 重试。401 TOKEN_EXPIRED 只允许一次静默刷新;刷新失败必须退出登录。403 不重试,429 按服务端退避。
状态:企业解析接口与 WebSocket 租户握手均已实现。
当前系统的执行方式:
host 和普通 API Key,可先调用 POST /v1/im/tenants/resolve 获取企业公开信息,再使用 wss://{host}/{api_path}v0/channels?apikey={api_key} 建立 WebSocket。{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"用户输入的企业码"}}。im_tenant.code 查询企业;成功返回 {ctrl code:201} 和 params.tenant{code,name},同时固定 Session 的内部 tenant_id。客户端保存服务端返回的标准 tenant.code/name,再进入 1.2 登录。TENANT_UNAVAILABLE 产品错误码。企业解析接口:
POST /v1/im/tenants/resolve 未登录可调用,但必须带 X-Tinode-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。hi(tenant) 做最终租户绑定;hi 成功后才进入 1.2。TENANT_UNAVAILABLE;客户端不得区分不存在和停用。网络失败保留输入并允许重试。成功响应示例:
{
"code": 0,
"data": {
"tenant": {"code": "acme", "name": "示例企业", "tenant_desc": null, "branding": {}},
"endpoints": {
"websocket": "wss://im.example.com/v0/channels",
"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"
}

状态:基本账号密码登录已实现;WS Token 可复用于产品 HTTP。
hi 的 WebSocket 发送 {login scheme:"basic"},secret=base64(username:password);不得先登录再传企业码。{ctrl params.user/token/expires/authlvl},Token 存 Keychain/Keystore;立即 sub me,再加载会话。前端不解码 Token,也不再次 Base64 编码。params.token 作为 Authorization: Bearer <params.token>;所有 /v1/im/* HTTP 请求仍必须带 X-Tinode-APIKey、X-IM-Schema: 1,写请求再带 Idempotency-Key。tenant_id/user_id/auth_version。修改密码、封禁账号后旧 Token 应失效。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 做程序判断。

状态:已实现基础错误,错误码和限流需改造。
login 返回 {ctrl code:401} 或产品错误 AUTH_FAILED。tenant + account + device/IP 限流,连续失败返回 AUTH_RATE_LIMITED 和 retry_after_seconds;日志仅记录账号哈希和 Request ID。
状态:Tinode {acc} 基础注册和验证码预校验已实现;短信供应商接入前固定测试码为 123456。
POST /v1/im/auth/verification-codes,必须带 X-Tinode-APIKey、X-Tenant-Code、X-IM-Schema: 1,Body {"scene":"register","phone":"+86138..."};成功返回 challenge_id、expires_in_seconds、resend_after_seconds。POST /v1/im/auth/verification-codes/{challenge_id}/verify,Body {"code":"123456"},获得一次性 verification_token。hi 后发送 {acc}。把一次性验证结果放在 {acc}.cred[0].resp,它必须是 HTTP verify 返回的 verification_token,不是短信验证码 123456。不得写入 desc.private,也不得在 {acc} 顶层传 tags。服务端会根据已校验的 cred 建立手机号凭证;客户端主动设置 tags:["tel:..."] 会被 Tinode 权限策略拒绝并返回 403 params.what=tags。{acc}.secret 中的 username、cred.val 必须使用同一个规范化号码,推荐 E.164 格式如 +86138...;不要一处使用 +86138...、另一处使用 86138...。
{"acc":{"id":"acc-1","user":"new","scheme":"basic","secret":"base64(+86138...:password)","login":true,"cred":[{"meth":"tel","val":"+86138...","resp":"verification_token","params":{"challenge_id":"challenge-id"}}],"desc":{"public":{"fn":"用户昵称"}}}}
user 和登录 Token;客户端保存 params.token,随后执行 sub me 和会话恢复。创建用户前原子校验 im_tenant_config.max_users,超限返回 TENANT_USER_QUOTA_EXCEEDED。
状态:新增。
resend_after_seconds 为准,并结合 server_time 计算,不能只依赖本地固定 60 秒。VERIFICATION_RATE_LIMITED 和剩余秒数。challenge_id 恢复;超过 expires_in_seconds 必须重新发送,旧验证码失效。
状态:新增。
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":"..."},带幂等键。auth_version、撤销该用户全部 access/refresh token 和在线 Session;客户端清理本租户凭证并返回登录页。PASSWORD_POLICY_VIOLATION 返回规则明细;验证码错误不返回账号存在性,达到上限后冻结 challenge。.png)
状态:新增。
POST /v1/im/security/challenges,Body {"scene":"close_youth_mode","channel":"sms"};手机号由登录身份读取,客户端不提交目标手机号。challenge_id 和脱敏手机号;提交 POST /v1/im/security/challenges/{id}/verify,Body {"code":"..."}。action_token、action、expires_at;后续敏感接口以 X-Action-Token 携带,Token 只能用于指定动作且单次消费。CHALLENGE_EXPIRED、CODE_INVALID、TOO_MANY_ATTEMPTS;前端不得把短信验证码或 action token 落普通存储。
状态:新增。
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 系统事件更新状态。
状态:新增。
GET /v1/im/security/realname。verified 时返回如 masked_name:"张*"、masked_id_number:"110***********1234"、verified_at。POST /v1/im/security/realname/appeals,需二次身份验证;不复用本页保存按钮。.png)
状态:新增。
GET /v1/im/bootstrap 返回 security.realname{status,required,allow_skip,remind_after};只有 required=true 且未认证时显示弹窗。allow_skip=true 时本地关闭,并按 remind_after 控制再次提示。REALNAME_REQUIRED,前端弹窗不能作为授权依据。.png)
状态:新增。
POST /v1/im/settings/independent-password,Body {"password":"1234","purpose":"youth_mode","action_token":"..."};服务端只存强哈希,不返回密码。POST /v1/im/settings/independent-password/verify 返回短时 action_token,用于关闭青少年模式或修改密码。TOO_MANY_ATTEMPTS 和 retry_after_seconds。前端输入框禁截屏/自动填充,离开页面立即清空。
状态:会话订阅已实现;产品摘要和系统入口需改造。
sub me,再通过 get 拉取 sub 元数据;每项使用 topic、public、private、updated、seq/read/recv 计算名称、摘要和未读数。{data} 更新;未读数为 max(seq-read,0),回执以服务端游标为准,不能只用本地计数。GET /v1/im/bootstrap 返回固定入口、功能开关、权限和系统 Topic;搜索调用 GET /v1/im/search/conversations?q=&cursor=。tenant_code + user_id 隔离。切租户、退出或账号变化时不得复用另一身份的会话摘要。
状态:需改造客户端恢复流程和消息幂等。
pending 队列。hi -> token login -> sub me -> 恢复 Topic -> get data。seq/read/recv 增量拉取。重发消息复用 x-im-client-mid,服务端需实现去重并返回原 seq。
状态:菜单为纯客户端;权限数据新增。
GET /v1/im/bootstrap 的 features 与 permissions。create_group=false 时隐藏“创建群聊”;user_search=false 时隐藏“添加好友”;扫码权限只影响实际扫码,不影响服务端授权。
状态:基础消息操作部分已实现;产品权限矩阵需改造。
type/from/seq/created_at/status、当前 Topic 权限和服务端下发的 message_policy 预裁剪菜单。{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=。所有最终操作由服务端验证消息归属、时限、群角色和租户。
状态:需改造。
GET /v1/im/system-notifications?cursor= 返回 id,event_type,title,summary,created_at,read_at,action;新通知通过服务端专用系统 Topic 的 system_event 消息推送。device_name,platform,ip_masked,login_at,security_action;IP 只下发必要脱敏值。POST /v1/im/system-notifications/{id}/read,带幂等键;跳转目标只接受服务端白名单 action.type 和内部参数。sys Topic 发布的路径必须封禁。客户端不得渲染普通用户伪造的系统卡片为可信通知。.png)
状态:需改造好友关系状态。
GET /v1/im/conversations/{topic}/context,返回 relationship=pending_inbound、can_send=false、对方公开资料和申请 ID。relationship_event 系统消息渲染;普通消息仍通过 Topic {data} 下行。pub 绕过,服务端发布路径必须校验好友关系。!个人对话框(对方的状态)(1).png)
状态:在线状态已具备基础能力;关系文案新增。
{pres} 更新在线/离线,last_seen 仅在对方隐私允许时展示。GET /v1/im/conversations/{topic}/context 返回 display_name,avatar_file_id,online_visibility,relationship。system_event{event_type:"friend_relationship_changed"},客户端不得根据本地动作自行永久插入系统记录。.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,禁止重复创建和普通消息发送。!个人对话框(我的状态)(1).png)
状态:发送和读回执已实现基础能力;送达状态映射需统一。
client_mid 后显示 sending;pub 收到 202 + seq 更新为 sent;失败保留同一 client_mid 重试。note what:"recv" 更新 delivered,note what:"read" 更新 read;多设备取服务端最大游标。failed(reason),不得伪装成已发送。
状态:新增好友审批与 Topic 联动。
friend_relationship_changed:accepted 系统事件。context.relationship=friend 后解锁输入区;不得仅因本地“通过”按钮成功动画解锁。since 拉取申请上下文和正常消息。
状态:上传基础已实现;媒体消息 Schema、ACL 和恢复需改造。
client_mid 和稳定占位;调用文件上传,得到 file_id 后再 pub x-im-type=image|video|voice。content 至少含 file_id,name,size,mime,width,height,duration_ms,thumbnail_file_id 中适用字段;上传进度是客户端任务状态,不写成聊天消息。pub 失败时复用已上传 file_id 和 client_mid。
状态:黑名单产品规则新增。
context.relationship=blocked_by_me|blocked_by_peer、can_send=false;服务端 pub 返回统一 MESSAGE_NOT_ALLOWED,不得向发送者泄露具体是对方拉黑。read 渲染;拉黑不能回滚既有回执。解除拉黑见 5.10、5.12。
状态:新增好友删除后的发送约束。
DELETE /v1/im/friends/{user_id};成功后双方关系变为 none,服务端推送关系变化事件。pub 返回 FRIEND_RELATION_REQUIRED;前端保留失败草稿,提供“重新添加好友”入口,不自动发申请。
状态:纯客户端。
tenant + user + topic 本地加密保存;若实现多端草稿,再单独调用 PUT /v1/im/topics/{topic}/draft,不得混入聊天消息。
状态:面板纯客户端;上传需改造。
context.capabilities 判断图片、视频、语音、文件是否允许,调用系统选择器/相机。mime,size 和 bootstrap.limits.max_file_size;服务端仍必须重新校验租户配额、文件类型和会话权限。!个人对话-发各种文件状态(1).png)
状态:需改造产品消息 Schema。
x-im-type 选择文本、名片、文件等卡片,公共字段统一使用 from,topic,seq,ts,head,content。
状态:新增产品消息类型。
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/{user_id}/profile;用户不可见或已离职时显示失效卡片,不跨租户查询。
状态:纯客户端。
tenant + user + topic 保存。context.can_send 并执行 pub text;正文去首尾策略、最大长度从 bootstrap.limits.max_text_length 获取,服务端再次校验。
状态:新增群成员查询。
@ 后调用 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 表示。
状态:纯客户端选择;发送协议需改造。
user_id,显示昵称只是快照。pub.head.mentions=[{"user_id":"usr...","offset":0,"length":3}];@所有人 使用 mention_all:true,不得伪造用户 ID。mention_all 权限;无效成员可删除提醒语义但保留可读文本,越权 @所有人 返回 MENTION_ALL_FORBIDDEN。
状态:需改造提醒和 Push。
{data} 下行,客户端使用结构化 mentions 高亮,禁止通过字符串扫描昵称判断。mentioned=true;读到对应 seq 后随 note read 清除。.png)
状态:纯客户端。
text 内容发送。自定义表情资源首版未定义,不得把本地资源路径直接发送。
状态:Unicode 表情使用已实现文本消息;独立表情包新增。
pub x-im-type:text 发送。独立自定义表情若启用,使用 x-im-type:"sticker",content={pack_id,sticker_id,file_id,width,height}。
状态:录音纯客户端;上传和语音消息需改造。
context.capabilities.voice;权限不足进入 10.21,不发请求。pub x-im-type:voice,内容含 file_id,duration_ms,size,mime,waveform。
状态:纯客户端。

状态:录音纯客户端;临时状态可选改造。
{note what:"kpa",topic};服务端限频,其他端超时自动消失,不能作为聊天记录。
状态:客户端任务管理新增;服务端上传需改造。
client_mid,local_uri,type,upload_id,progress,state;同一文件可分片/断点续传时使用稳定 upload_id。FILE_TOO_LARGE、FILE_TYPE_NOT_ALLOWED、STORAGE_QUOTA_EXCEEDED 时不可自动无限重试。
状态:需改造媒体 Schema 和受权下载。
duration_ms,waveform,未听状态保存在客户端并可选跨端同步;视频渲染 thumbnail_file_id,duration_ms,width,height;图片渲染缩略图和原图 ID。FILE_UNAVAILABLE。
状态:需改造。
client_mid 或列表位置。
状态:纯客户端视觉资源。
purpose 以便服务端执行权限和保留期策略。
状态:上传已实现基础能力;消息引用和 ACL 需改造。
x-im-type:file,content={file_id,name,size,mime,extension,checksum};文件名做长度和控制字符清理。client_mid,某个失败不能改变其他文件状态。
状态:选择纯客户端;批量动作新增。
(topic,seq) 保存已选项并显示数量;默认最多 100 条,超限在本地阻止。succeeded[] 与 failed[{seq,reason}]。
状态:新增。
POST /v1/im/favorites/batch,Body {"messages":[{"topic":"grp...","seq":"123"}]},带幂等键。created_count,existing_count,failed[];同一用户对同一 topic+seq 重复收藏不新增记录。
状态:新增。
GET /v1/im/forward-targets?query=&cursor=,只返回 can_receive_forward=true 的会话和文件助手。
状态:新增。
bootstrap.limits.max_forward_targets,首版建议 9。tenant_id。跨租户 Topic 在服务端统一拒绝。
状态:新增。
POST /v1/im/messages/forward,Body {"mode":"separate","sources":[{"topic":"p2p...","seq":"12"}],"target_topics":["grp..."],"comment":"..."}。client_mid/seq/status;服务端保持源顺序,重新生成目标消息,不能复用原 seq。
状态:新增。
mode:"bundle";服务端生成 forward_bundle,内容含 bundle_id,title,item_count,preview[],完整内容通过 GET /v1/im/forward-bundles/{bundle_id}?cursor= 获取。
状态:新增产品通讯录接口。
GET /v1/im/friends?query=&initial=&cursor=,返回 user_id,display_name,remark,avatar_file_id,initial,online_visibility;显示名优先级为备注名、昵称、账号。next_cursor 当前按最后一条好友的 peer_id 生成,客户端必须作为不透明字符串原样回传,不要自行解析、排序或拼接。GET /v1/im/organization/members?query=&department_id=&cursor=;好友和组织成员是两种关系,前端不得混为一个可发消息权限。GET /v1/im/bootstrap 返回。列表缓存以租户和用户隔离。
状态:新增。
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 和 P2P topic。重复审批返回原结果;并发冲突返回 REQUEST_ALREADY_HANDLED 并附当前状态,前端刷新该项。
状态:新增。
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。allow_user_search 和用户隐私再次拦截。!添加好友(1).png)
状态:新增。
POST /v1/im/me/qrcodes,Body {"purpose":"add_friend","expires_in_seconds":86400},返回 qr_token,qr_payload,expires_at,display_profile。QR_CODE_EXPIRED,篡改返回 QR_CODE_INVALID。
状态:新增。
GET /v1/im/users/search?q=&type=exact_account;手机号查询仅当租户和目标隐私同时允许时使用 type=phone。action=add|chat|pending|none。前端不得根据是否返回手机号推断账户存在。SEARCH_RATE_LIMITED;禁止客户端批量枚举 ID。
状态:新增。
POST /v1/im/friend-requests,Body {"target_user_id":"usr...","message":"验证留言","source":"search|qr|group","source_topic":"grp..."}。source_topic 只能是双方当前可见的同租户群。request_id,status:"pending";重复有效申请返回原记录,目标隐私关闭时返回统一 FRIEND_REQUEST_NOT_ALLOWED。
状态:新增。
GET /v1/im/users/{user_id}/profile 返回按当前查看人裁剪后的 display_name,remark,avatar_file_id,gender,region,public_account,signature,relationship,capabilities。topic;没有 Topic 时调用 POST /v1/im/conversations/p2p,Body {"peer_user_id":"..."} 并由服务端验证好友关系。capabilities.can_call 展示,真正呼叫时再次鉴权。隐藏字段必须从响应中省略,不能只由前端遮挡。
状态:新增。
GET /v1/im/groups/{topic}/members/{user_id}/profile 返回群内昵称、角色和允许公开的账号资料。can_add_friend 同时受群 allow_member_add_friend、目标隐私和租户策略控制;为 false 时隐藏添加入口。source:"group" 和当前 Topic;服务端校验双方仍在群内。
状态:部分 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。
状态:新增。
PUT /v1/im/blocks/{user_id};解除:DELETE /v1/im/blocks/{user_id},均带幂等键。blocked,relationship,updated_at 并向当前用户其他设备推送 privacy.block.updated。拉黑后服务端拦截对方消息和呼叫。relationship 仍为 none,不能自动恢复好友。
状态:确认框纯客户端;删除接口新增。
DELETE /v1/im/friends/{user_id},带幂等键。是否清除本地历史由客户端在成功后独立处理,不放入删除好友请求。relationship:"none",topic。当前用户是否删除本地/服务端可见记录是独立选择。
状态:新增。
GET /v1/im/blocks?cursor= 返回 user,blocked_at;移出调用 5.10 的 DELETE。POST /v1/im/blocks/batch-delete,Body {"user_ids":[...]},返回逐项结果;最多 100 人。!黑名单提示(1).png)
状态:确认框纯客户端;写接口新增。
PUT /v1/im/blocks/{user_id},相同幂等键重复提交返回当前 blocked=true。RESOURCE_NOT_FOUND。
状态:图片文件名与画面语义不一致;按“清除聊天记录”实施,需改造。
{del what:"msg",topic,delseq:[{"low":1,"hi":last_seq+1}],hard:false},仅删除当前用户视图;或封装 DELETE /v1/im/conversations/{topic}/messages。
状态:Tinode 群 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":"...","member_user_ids":[],"avatar_file_id":"..."};原子校验租户群数上限和群成员上限,返回 topic。my_role 决定;创建成功或角色变化后通过系统事件刷新,不由前端自行移动分组。
状态:纯客户端。
items=[] 且 has_more=false 时显示对应空状态,不追加空白占位数据。group.membership.updated 或创建成功后重新拉取当前分类。
状态:新增邀请卡片和加入接口。
system_event{event_type:"group_invitation",invitation_id,group,inviter,status,expires_at}。POST /v1/im/group-invitations/{invitation_id}/accept,带幂等键;返回 status=joined|pending_approval 和 topic。INVITATION_EXPIRED。
状态:新增群成员事件。
POST /v1/im/groups/{topic}/invitations,Body {"user_ids":[],"message":""};按群策略直接加入或创建待审批申请。system_event{event_type:"group_membership_changed",operator,target_users,action,result} 并写入群时间线。
状态:新增。
GET /v1/im/groups/{topic}/join-requests?status=pending&cursor= 返回申请人、邀请人、来源、时间、状态和 version。POST /v1/im/groups/{topic}/join-requests/{id}/accept;拒绝:POST /v1/im/groups/{topic}/join-requests/{id}/reject,带幂等键与 version。REQUEST_ALREADY_HANDLED 和最终状态。
状态:新增。
GET /v1/im/groups/{topic}/summary 返回 pending_join_request_count、最新公告和权限;新申请通过 group.join_request.created 推送给有审批权限的成员。
状态:新增。
GET /v1/im/groups/{topic}/announcements/latest;发布/更新:PUT /v1/im/groups/{topic}/announcements/current,Body {"content":"...","version":1}。announcement_id,content,author,version,published_at。仅有权限角色可写,使用乐观锁防覆盖。
状态:纯客户端组合;数据接口新增。
GET /v1/im/groups/{topic}/summary,分别读取 announcement 和 pending_join_request_count,按产品固定顺序渲染两个入口。.png)
状态:新增。
GET /v1/im/groups/{topic}/announcements/{announcement_id};普通成员响应不含编辑权限。POST /v1/im/groups/{topic}/announcements/{id}/read,带幂等键,返回 read_at;用于清除新公告标记。
状态:部分订阅设置已实现;产品菜单新增。
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。
状态:需改造。
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 允许的入口。
状态:上传基础已实现;用途绑定需改造。
purpose=group_avatar,服务端校验图片 MIME、尺寸、大小并返回 file_id。{"avatar_file_id":"...","version":n};只有 PATCH 成功才替换正式头像。!设置昵称(1).png)
状态:新增。
PUT /v1/im/groups/{topic}/members/me/profile,Body {"nickname":"...","version":n}。nickname,version,updated_at 并推送成员资料变化。
状态:同 6.11,需改造。
PATCH /v1/im/groups/{topic},Body {"description":"...","version":n};空字符串表示清空,省略表示不修改。VALIDATION_FAILED 的 details.field/max_length 或 CONTENT_REJECTED;前端保留草稿供修正。
状态:角色已有基础数据;产品渲染需统一。
sender_snapshot{display_name,role},角色枚举固定 owner|admin|member。
状态:新增。
POST /v1/im/groups/{topic}/qrcodes,Body {"expires_in_seconds":86400},返回签名 qr_token,qr_payload,expires_at,group_snapshot。POST /v1/im/qrcodes/resolve,purpose:"join_group";随后 POST /v1/im/groups/{topic}/join-requests 并携带 qr_token。
状态:新增。
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、二维码、邀请和文件新增权限。
状态:新增。
GET /v1/im/groups/{topic}/members?role=admin&cursor= 返回管理员列表;GET /v1/im/groups/{topic}/role-limits 返回 max_admin_count,current_admin_count。owner 单独展示,不计入普通管理员列表和名额。
状态:新增。
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。
状态:新增。
admins,version,updated_at;前端用响应替换列表并显示成功,不在请求完成前自行修改角色。group.role.updated,新管理员收到后刷新菜单权限和群标识。!添加管理员-设置成功(1).png)
状态:新增。
succeeded[]、failed[{user_id,reason}] 和最新 admins。
状态:新增。
DELETE /v1/im/groups/{topic}/admins/{user_id},带幂等键和 If-Match/version。member,保留群成员关系;服务端立即撤销管理权限并广播角色事件。
状态:新增。
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":""}。
状态:新增。
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;不能只禁用前端输入框。
状态:新增。
GET /v1/im/groups/{topic}/members?eligible_role=owner&query=&cursor=,只返回状态正常的当前成员。target_user_id;当前群主、已退出、停用账号返回 eligible=false。
状态:新增。
POST /v1/im/groups/{topic}/owner-transfer,Body {"target_user_id":"usr...","version":n},必须带幂等键和二次认证 X-Action-Token。owner,previous_owner,roles,version;并发成员变化返回 GROUP_VERSION_CONFLICT,前端重新确认,不自动重试。
状态:新增群文件索引;底层文件存储已实现。
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} 需上传者或管理员权限。
状态:需改造。
purpose=group_file,topic=... 上传,再 POST /v1/im/groups/{topic}/files,Body {"file_id":"..."} 建立群文件记录。member_upload_files、单文件上限、租户总存储和群成员身份;成功返回完整群文件项。upload_id;登记失败不得在列表显示,孤立文件按 TTL 回收。
状态:新增。
GET /v1/im/favorites?type=all|text|image|video|voice|file|link&q=&cursor=,返回 favorite_id,type,snapshot,source,collected_at,availability。source 仅下发用户仍可知晓的会话摘要。!我的收藏(1).png)
状态:新增。
GET /v1/im/favorites/{favorite_id};删除:DELETE /v1/im/favorites/{favorite_id};转发使用 7.8。availability:"removed"。
状态:新增。
type=file;项中使用 snapshot.file{name,size,mime,file_id} 和 availability。
状态:新增。
type=text;服务端返回纯文本/结构化 Drafty 快照和安全高亮区间 matches[]。permissions.can_copy。
状态:新增。
type=voice;播放前获取认证文件,返回 duration_ms,waveform,availability。
状态:新增。
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。.png)
状态:新增。
POST /v1/im/favorites/batch-delete,Body {"favorite_ids":[]};批量转发见 7.8。
状态:新增。
POST /v1/im/favorites/forward,Body {"favorite_ids":[],"target_topics":[],"comment":""}。
状态:底层下载已实现租户校验;资源 ACL 需改造。
GET /v0/file/s/{file_id},Header 传 API Key 与 X-Tinode-Auth: token <base64_token>;禁止 ?auth=&secret=。Range、ETag、内容长度和安全文件名。FILE_FORBIDDEN 不自动重试,FILE_UNAVAILABLE 显示失效。
状态:客户端任务管理;服务端需支持 Range。
file_id,etag,total_bytes,downloaded_bytes,temp_path,state;恢复时携带 Range 与 If-Range: <etag>。
状态:纯客户端完成态。
.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 并高亮。
状态:新增。
GET /v1/im/topics/{topic}/messages/search?type=file&q=&cursor=,返回消息 seq 与文件快照、发送者、时间、availability。
状态:新增。
type=link,返回 url,title,summary,seq,ts,risk_level;打开前执行 7.6 URL 安全检查。
状态:新增。
GET /v1/im/topics/{topic}/media?type=image&cursor= 返回 seq,thumbnail_file_id,file_id,width,height,ts,availability。(topic,seq),操作使用第 4 章批量接口。
状态:新增。
type=video,额外返回 duration_ms,mime,size。
状态:已实现基础通话记录替换;产品文案映射需统一。
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。
状态:在线邀请已实现;Push 与产品鉴权需改造。
{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"。callId,topic,seq,caller_display,callType,expires_at,不含 SDP、ICE 或 Token;用户操作后必须回 WebSocket 鉴权。
状态:发起与超时已实现基础;好友/拉黑/租户开关校验需改造。
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。
状态: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 恢复或结束,不新建第二通呼叫。
状态:纯客户端设备控制。
media-state,不落聊天记录。
状态:纯客户端设备控制。

状态:基础通话信令已实现;隐私约束需落实。
media_type:"video"。本地预览只在设备显示,接听前媒体轨默认不发给对方,SDP 只声明能力。hang-up:cancelled。相机权限不足时改为音频呼叫或取消,不伪装视频已开启。
状态:纯客户端状态;媒体状态事件需改造。
video_enabled=false。ringing/accept/offer/answer/ice-candidate/hang-up;首版应直接通过 WebRTC 轨道 mute 状态呈现。若后续新增 media-state,必须先修改 server/calls.go 事件白名单、参与者校验和限频,客户端不能提前发送未知事件。
状态:WebRTC 客户端能力;服务端信令已具备基础。
hang-up。服务端未支持 media-state 前不得发送该事件。
状态:现阶段纯客户端;可选媒体状态事件需改造。
media-state 必须等服务端扩展事件白名单后另行启用,且不持久化、不改变通话结果。
状态:纯客户端。
call_id 运行;点击恢复全屏不发送新 invite。hang-up、Token 失效或媒体会话结束后立即关闭悬浮窗并落正确结果态。
状态:新增。
GET /v1/im/workbench 返回 modules[{code,name,icon_file_id,route,unread_count,enabled,permission}]、布局版本和企业配置。route,禁止直接执行服务端任意 URL。workbench.badge.updated 系统事件增量更新;切租户必须重新加载,不复用另一企业工作台。
状态:新增。
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=。服务端执行组织范围授权,离职用户标记不可联系或从结果移除。
状态:新增。
POST /v1/im/meetings,Body {"title":"...","start_at":"...","end_at":"...","location":"...","attendee_user_ids":[],"reminder_minutes":[]}。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。所有写操作带幂等键并生成通知和审计。
状态:新增。
PUT /v1/im/meetings/{meeting_id}/my-response,Body {"response":"accepted|declined|tentative","version":n}。MEETING_CLOSED 并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。
状态:新增。
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 和可补交日期由服务端返回,前端不自行判断工作日规则。
状态:新增。
PUT /v1/im/reports/daily/{id}/draft,Body {"content":"...","attachments":[],"version":n};提交 POST /v1/im/reports/daily/{id}/submit。REPORT_VERSION_CONFLICT 并给最新版本。editable_until 决定是否可修改;附件使用 purpose=daily_report 上传并做权限绑定。
状态:新增。
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":[]};服务端过滤无管理权限对象并限流,返回逐项结果。
状态:新增。
GET /v1/im/reports/daily/{report_id} 返回填报人、日期、内容、附件、状态、版本、修改时间和 permissions。revised_at,不能混合旧缓存。!月报(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。
状态:新增。
GET /v1/im/reports/monthly?scope=mine|managed&from_month=&to_month=&status=&cursor= 返回月份、填报人、状态和时间。GET /v1/im/reports/monthly/statistics?month=&department_id=;普通用户响应中不下发团队统计入口权限。action=edit|view 决定。
状态:新增。
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 或审核流程,不直接覆盖。
状态:新增。
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":[]},带幂等键。submission_id,status:"received",submitted_at;匿名模式下业务接收方不见身份,但安全审计是否保留内部追踪按企业政策执行并提前告知。
状态:新增配置接口。
notice{title,content,updated_at},由租户后台配置;客户端不得硬编码实名/匿名承诺。notice_version,服务端发现关键政策已变更时返回 NOTICE_RECONFIRM_REQUIRED。
状态:基础用户资料已有;产品菜单新增。
GET /v1/im/me 返回公开账号资料、实名/青少年模式摘要和资料版本;GET /v1/im/bootstrap 返回个人菜单与权限。
状态:Tinode me 资料更新已有基础;目标产品接口需改造。
GET /v1/im/me/profile 返回 avatar_file_id,nickname,birth_date,signature,gender,region,public_account,version,field_rules。PATCH /v1/im/me/profile,仅提交变更字段并带 version;服务端更新后同步 Tinode me.public 并推送资料事件。purpose=user_avatar 上传再 PATCH;任何字段的可见性由隐私策略裁剪。
状态:需改造。
PATCH /v1/im/me/profile,Body {"nickname":"新昵称","version":n}。nickname,version,updated_at。
状态:新增产品字段。
{"birth_date":"2000-01-02","version":n};仅传 ISO 日期,不传本地化文本或时区时间戳。PROFILE_FIELD_INVALID。展示格式由客户端本地化。
状态:需改造。
{"signature":"...","version":n};空字符串表示清空。field_rules.signature.max_length 和内容审核处理;失败保留本地草稿,成功刷新引用页面。
状态:图标纯客户端;二维码协议新增。
POST /v1/im/me/qrcodes 获取新 Token。user_id、手机号或登录 Token;过期、撤销和切租户后必须重新生成。.png)
状态:菜单纯客户端;数据接口部分新增。
bootstrap.settings_menu,安全、通知、隐私、通用、关于按权限展示。系统通知权限状态由 iOS API 读取,不由服务端伪造。POST /v1/im/auth/logout 撤销当前 refresh/access token 和设备会话,成功或 Token 已失效后关闭 WS、清安全存储和本租户用户缓存。DELETE /v1/im/security/sessions/{session_id},需二次验证。.png)
状态:菜单纯客户端;业务协议同 10.7。
bootstrap.settings_menu 与退出协议;Android 系统权限、通知渠道和应用更新使用平台实现。
状态:纯客户端。

状态:新增 App 配置接口。
GET /v1/im/app/releases/latest?platform=ios|android¤t_version=1.0.0&channel=stable,未登录可在企业解析后调用。latest_version,min_supported_version,update_type=none|optional|required,release_notes,store_url,package_url,checksum,published_at。
状态:纯客户端弹窗;数据来自 10.10。
optional 显示稍后/更新,记录本地已忽略版本并按服务端 remind_after 再提示;required 不显示绕过入口。
状态:新增。
GET /v1/im/me/privacy 返回 add_me_methods,profile_visibility,last_seen_visibility,read_receipts,version 和各字段 editable。PATCH /v1/im/me/privacy 只提交变更字段和 version;成功返回完整最新配置并向其他设备推送 privacy.updated。
状态:新增。
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,前端禁用并显示服务端提供的通用说明。.png)
状态:新增。
GET /v1/im/settings/youth-mode 返回 enabled,policy_version,restrictions,password_set,can_enable。POST /v1/im/settings/youth-mode/enable,Body {"policy_version":n,"action_token":"..."}。.png)
状态:弹窗纯客户端;文案由服务端配置。
restrictions 与 policy_version 渲染;取消不请求,确认进入密码设置/验证。policy_version;若策略更新返回 POLICY_RECONFIRM_REQUIRED,重新展示新内容。.png)
状态:新增。
enabled=true 时展示 enabled_at,restrictions。bootstrap 同时返回受限模块和动作。YOUTH_MODE_RESTRICTED;前端隐藏只是体验优化,不能作为安全控制。youth_mode.updated 即刷新。.png)
状态:新增。
action_token;再 POST /v1/im/settings/youth-mode/disable,Header X-Action-Token,带幂等键。enabled:false,updated_at 并广播;连续密码错误按服务端 retry_after_seconds 锁定。.png)
状态:权限引导纯客户端;设备注册需改造。
PUT /v1/im/devices/{device_id}/push-token,Body {"provider":"fcm","token":"...","app_version":"..."};Token 轮换时覆盖。.png)
状态:纯客户端。
.png)
状态:纯客户端。
.png)
状态:纯客户端。
.png)
状态:新增风控决策协议。
409 RISK_CONFIRMATION_REQUIRED,details={risk_challenge_id,level,notice_code,notice_params,expires_at,action}。notice_code 映射经过审核的本地文案,不显示服务端规则、分数或命中特征;取消不重试原操作。security.risk_notice 系统事件触发。普通消息不得伪装成系统反诈弹窗。.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 绕过。
状态:新增。
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 或被举报者任意文本身份。!举报(1).png)
状态:新增。
purpose=abuse_evidence 上传;提交 POST /v1/im/reports/abuse,Body {"target":{"type":"message","topic":"...","seq":"12"},"reason_code":"fraud","description":"...","evidence_file_ids":[]}。report_id,status:"received"。REPORT_RATE_LIMITED;证据使用独立严格 ACL 和保留期。
状态:新增。
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",重复提交由幂等键去重。/v0/channels、Wire Version 0.25 和最低版本 0.20。hi 携带企业码并固定 Session 租户;响应包含公开租户信息、限制、ICE 配置和呼叫超时。basic/token 登录,sub/get/set/pub/leave/del/note 基础 Tinode 协议。pub 成功返回 202 + seq,Topic 消息与基础回执同步。/v0/file/u/ 和 /v0/file/s/{file_id} 的基础上传下载及租户存储隔离。| 优先级 | 改造项 | 阻塞图片章节 |
|---|---|---|
| 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、11.4~11.5 |
| P1 | 好友申请、好友关系、黑名单状态机 | 第 3、5 章 |
| P1 | 群成员、角色、禁言、公告、邀请审批和配额 | 第 6 章 |
| P1 | 收藏、转发和聊天记录搜索 | 第 4、7 章 |
| P1 | 通话媒体状态、结果消息、Push 来电 | 第 8 章 |
| P2 | 工作台、组织、会议、日报、月报、信箱 | 第 9 章 |
| P2 | 隐私、青少年模式、反诈、举报、反馈 | 第 10、11 章 |
| 编号 | 场景 | 预期结果 |
|---|---|---|
| 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 过期后刷新并 Range 续传 | 文件完整、checksum 正确、不重复下载已完成分片 |
| T-11 | 风控二次确认后重复重放 risk token | 只有首次有效,其余返回 Token 已消费 |
| T-12 | 切换企业后查看缓存、上传任务、收藏和搜索 | 不出现上一租户数据或任务 |
首版客户端至少实现: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 原样展示给用户。
每个图片小节进入联调前,服务端须提供 OpenAPI/JSON Schema、错误码、权限规则和可用测试租户;前端须提供请求日志脱敏、断线/重试行为和四态页面(加载、空、成功、失败)。联调完成必须逐图勾选对应协议、至少覆盖成功、权限拒绝、重复提交、断网恢复和跨租户五类场景。