1559 lines
142 KiB
HTML
1559 lines
142 KiB
HTML
<!doctype html><html><head><meta charset="utf-8"><title>IM-前后端对接文档</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,"PingFang SC","Microsoft YaHei",sans-serif;line-height:1.65;color:#1f2937;max-width:1100px;margin:32px auto;padding:0 24px}h1{font-size:32px;border-bottom:1px solid #e5e7eb;padding-bottom:12px}h2{margin-top:36px;font-size:26px;border-bottom:1px solid #eef2f7;padding-bottom:8px}h3{margin-top:28px;font-size:20px}h4{margin-top:22px;font-size:17px}table{border-collapse:collapse;width:100%;margin:14px 0;display:block;overflow-x:auto}th,td{border:1px solid #d1d5db;padding:8px 10px;vertical-align:top}th{background:#f3f4f6}pre{background:#111827;color:#e5e7eb;padding:14px;border-radius:6px;overflow:auto}code{background:#f3f4f6;padding:2px 4px;border-radius:4px}pre code{background:transparent;padding:0}blockquote{border-left:4px solid #93c5fd;margin:12px 0;padding:6px 14px;background:#eff6ff;color:#374151}img{max-width:360px;width:100%;border:1px solid #e5e7eb;border-radius:6px}figure{margin:18px 0}figcaption{font-size:13px;color:#6b7280;margin-top:6px}a{color:#2563eb;text-decoration:none}</style></head><body>
|
||
<h1>IM 前后端逐图对接协议</h1>
|
||
<blockquote>版本:v2.0 </blockquote>
|
||
<blockquote>日期:2026-08-11 </blockquote>
|
||
<blockquote>需求依据:<a href="./IM-产品原型需求文档.md">IM 产品原型需求文档</a> </blockquote>
|
||
<blockquote>设计依据:<a href="./IM-系统设计文档.md">IM 系统设计文档</a> </blockquote>
|
||
<blockquote>说明:第 1 至第 11 章的章节编号、图片标题和顺序与产品原型完全一致。每张图均给出可直接开发、联调和验收的协议。</blockquote>
|
||
<h2>0. 公共对接约定</h2>
|
||
<h3>0.1 实现状态</h3>
|
||
<table><thead><tr><th>状态</th><th>含义</th></tr></thead><tbody>
|
||
<tr><td><code>已实现</code></td><td>当前服务端已有能力,可按本文联调</td></tr>
|
||
<tr><td><code>需改造</code></td><td>已有基础能力,但字段、鉴权或业务规则需要调整</td></tr>
|
||
<tr><td><code>新增</code></td><td>当前服务端没有该产品接口或数据模型</td></tr>
|
||
<tr><td><code>纯客户端</code></td><td>界面或系统能力,不调用服务端;关联的提交协议会单独指出</td></tr>
|
||
</tbody></table>
|
||
<p>文档中所有 <code>新增</code>、<code>需改造</code> 接口都是目标契约,不代表当前代码已经可用。前后端必须以本文字段为准实施,接口完成前可使用同结构 Mock。</p>
|
||
<h3>0.2 API Key、启动入口与通道</h3>
|
||
<h4>0.2.1 现有系统的 API Key 从哪里来</h4>
|
||
<p><strong>API Key 不是客户端启动后向 IM 服务端申请得到的值。现有代码没有“获取 API Key”的 HTTP 或 WebSocket 接口。</strong></p>
|
||
<p>API Key 是应用级接入标识,由部署人员在发布客户端前生成并配置:</p>
|
||
<ol>
|
||
<li>新部署同时生成 API Key 和 HMAC Salt:在项目根目录执行 <code>go run ./keygen</code>。</li>
|
||
<li>已有服务端继续使用当前 <code>api_key_salt</code> 时,执行 <code>go run ./keygen -sequence <新序号> -salt '<服务端 api_key_salt>'</code> 生成兼容的普通应用 Key。</li>
|
||
<li>输出中的 <code>HMAC salt</code> 配置到服务端 <code>server/tinode.conf</code> 的 <code>api_key_salt</code>;输出中的 <code>API key</code> 配置到 iOS、Android 或 Web 客户端构建配置/受控环境配置。</li>
|
||
<li>客户端启动时从自己的应用配置读取 API Key,不调用 IM 接口获取。禁止把 <code>-isroot 1</code> 生成的 Root Key 放进客户端。</li>
|
||
</ol>
|
||
<p>生成示意:</p>
|
||
<pre><code class="language-text">
|
||
$ go run ./keygen
|
||
API key v1 seq1 [ordinary]: <client-api-key>
|
||
HMAC salt: <server-hmac-salt>
|
||
</code></pre>
|
||
<p>API Key、企业码和用户 Token 是三个不同概念:</p>
|
||
<table><thead><tr><th>值</th><th>取得时间</th><th>作用</th><th>是否代表用户身份</th></tr></thead><tbody>
|
||
<tr><td><code>api_key</code></td><td>应用发布/环境配置时预置</td><td>允许应用访问 Tinode 入口</td><td>否</td></tr>
|
||
<tr><td><code>tenant_code</code></td><td>用户输入企业码</td><td>将 Session 路由并绑定到企业</td><td>否</td></tr>
|
||
<tr><td><code>token</code></td><td>用户登录成功后由服务端返回</td><td>标识当前租户内的登录用户</td><td>是</td></tr>
|
||
</tbody></table>
|
||
<p>API Key 可以被客户端应用提取,因此不能把它当成用户密码或租户授权。现有 <code>checkAPIKey</code> 只校验 Key 的格式和 HMAC 签名;最终权限仍必须依赖租户 Session、用户 Token 和 Topic/业务 ACL。</p>
|
||
<p>当前 <code>checkAPIKey</code> 没有按 <code>sequence</code> 吊销旧 Key 的逻辑。生成新序号不会自动让旧 Key 失效;若需要精细轮换,服务端必须增加最低可接受序号或撤销表。直接更换 <code>api_key_salt</code> 会让所有旧 Key 同时失效,必须与客户端升级协调。</p>
|
||
<h4>0.2.2 客户端怎样携带 API Key</h4>
|
||
<table><thead><tr><th>通道</th><th>地址</th><th>认证</th><th>用途</th></tr></thead><tbody>
|
||
<tr><td>WebSocket(浏览器/通用方式)</td><td><code>wss://{host}/{api_path}v0/channels?apikey={api_key}</code></td><td>Upgrade 校验 API Key,<code>hi</code> 绑定租户,<code>login</code> 登录</td><td>Topic、消息、回执、在线状态、通话信令</td></tr>
|
||
<tr><td>WebSocket(支持自定义 Header 的原生客户端)</td><td><code>wss://{host}/{api_path}v0/channels</code></td><td>Upgrade Header <code>X-Tinode-APIKey</code></td><td>同上</td></tr>
|
||
<tr><td>产品 HTTP</td><td><code>https://{host}/{api_path}v1/im/*</code></td><td><code>Authorization: Bearer <params.token></code></td><td>好友、群、收藏、搜索、工作台和设置</td></tr>
|
||
<tr><td>文件 HTTP</td><td><code>POST /v0/file/u/</code>、<code>GET /v0/file/s/{file_id}</code></td><td>当前用 <code>X-Tinode-Auth: token <base64_token></code></td><td>文件上传下载;禁止 Query 传 Token</td></tr>
|
||
</tbody></table>
|
||
<p>HTTP 请求统一通过 <code>X-Tinode-APIKey</code> 携带。浏览器 WebSocket 无法设置自定义 Upgrade Header 时才使用 <code>apikey</code> Query;日志和监控必须对该参数脱敏。产品 HTTP 还必须带 <code>X-IM-Schema: 1</code> 和 <code>X-Request-ID</code>;写操作带 <code>Idempotency-Key</code>。生产环境只允许 HTTPS/WSS。Token、密码、验证码、证件号不得写入 URL、普通日志、埋点或崩溃报告。</p>
|
||
<p>WebSocket Upgrade 阶段由 <code>server/hdl_websock.go</code> 先调用 <code>checkAPIKey</code>。缺少或无效时不会建立 WebSocket,直接返回 HTTP 403:</p>
|
||
<pre><code class="language-json">
|
||
{"ctrl":{"code":403,"text":"valid API key required","ts":"2026-08-11T08:30:15.123Z"}}
|
||
</code></pre>
|
||
<p>客户端应把该错误视为应用配置/环境配置错误,停止自动重连并上报脱敏诊断;它不是账号密码错误,也不能通过重新登录解决。</p>
|
||
<p>两种连接方式的第一个请求对照如下:</p>
|
||
<table><thead><tr><th>连接方式</th><th>发起第一个网络请求前已有的值</th><th>第一个网络请求</th><th>第一个响应得到的值</th><th>下一步</th></tr></thead><tbody>
|
||
<tr><td>WebSocket(推荐)</td><td><code>host + api_key + tenant_code</code></td><td>带 API Key Upgrade <code>/v0/channels</code></td><td>HTTP 101 和已建立的 WebSocket;不会返回新 API Key 或 sid</td><td>发送 <code>{hi tenant}</code></td></tr>
|
||
<tr><td>Long Polling(兼容)</td><td><code>host + api_key + tenant_code</code></td><td><code>POST /v0/channels/lp</code></td><td>HTTP 201 和 <code>ctrl.params.sid</code></td><td>带 <code>sid</code> POST <code>{hi}</code>,再 GET 等待响应</td></tr>
|
||
</tbody></table>
|
||
<p>现有实现对应关系:</p>
|
||
<table><thead><tr><th>源码</th><th>作用</th></tr></thead><tbody>
|
||
<tr><td><code>keygen/keygen.go</code></td><td>生成普通/Root API Key 和服务端 HMAC Salt</td></tr>
|
||
<tr><td><code>server/api_key.go</code></td><td><code>checkAPIKey</code> 校验 API Key 格式与签名</td></tr>
|
||
<tr><td><code>server/http.go</code></td><td>按 Header、Query、Form、Cookie 顺序读取 API Key</td></tr>
|
||
<tr><td><code>server/hdl_websock.go</code></td><td>WebSocket Upgrade 前校验 API Key,无效直接 403</td></tr>
|
||
<tr><td><code>server/hdl_longpoll.go</code></td><td>创建 Long Polling Session 并返回 <code>sid</code></td></tr>
|
||
<tr><td><code>server/session.go</code></td><td>处理 <code>{hi}</code> 租户绑定、<code>{login}</code> 和登录 Token 返回</td></tr>
|
||
</tbody></table>
|
||
<p>现有 API Key 是应用级、非租户级,同一个普通应用 Key 可以进入不同企业;具体租户只能由后续 <code>{hi}.tenant</code> 决定。若将来要求每个租户独立 API Key,需要新增 Key 与租户绑定关系,不能只修改客户端配置。</p>
|
||
<h3>0.3 多租户强制规则</h3>
|
||
<ol>
|
||
<li>未登录阶段只传公开的 <code>tenant_code</code>;客户端永远不接收、不提交数据库 <code>tenant_id</code>。</li>
|
||
<li>WebSocket 第一条消息必须是 <code>{hi}</code>,其中 <code>tenant</code> 为企业码。服务端解析后把租户固定到 Session。</li>
|
||
<li>Token 内部绑定租户。Token 租户与当前 Session 租户不一致时返回 <code>TENANT_MISMATCH</code>,不得自动切换。</li>
|
||
<li>切换企业必须退出登录、关闭 WebSocket、清除该租户的 Token/缓存,再重新解析企业码和握手。</li>
|
||
<li>已登录 HTTP 请求的租户和用户只从 Token Principal 获取。请求体中的 <code>tenant_id</code>、<code>user_id</code> 不能覆盖身份。</li>
|
||
<li>跨租户对象统一返回 <code>RESOURCE_NOT_FOUND</code> 或 <code>FORBIDDEN</code>,不得暴露对象是否存在。</li>
|
||
</ol>
|
||
<h3>0.4 HTTP 请求与响应</h3>
|
||
<p>成功响应统一为:</p>
|
||
<pre><code class="language-json">
|
||
{
|
||
"code": 0,
|
||
"data": {},
|
||
"request_id": "01K...",
|
||
"server_time": "2026-08-11T08:30:15.123Z"
|
||
}
|
||
</code></pre>
|
||
<p>失败响应统一为:</p>
|
||
<pre><code class="language-json">
|
||
{
|
||
"code": 30001,
|
||
"reason": "GROUP_MUTED",
|
||
"message": "permission denied",
|
||
"details": {},
|
||
"request_id": "01K...",
|
||
"server_time": "2026-08-11T08:30:15.123Z"
|
||
}
|
||
</code></pre>
|
||
<p>客户端只使用稳定的 <code>reason</code> 分支和本地化文案,不解析 <code>message</code>。ID、Topic、Seq 游标在客户端均按字符串处理;时间统一为 UTC RFC3339 毫秒。分页统一使用 <code>limit</code> 与不透明 <code>cursor</code>,返回 <code>items</code>、<code>next_cursor</code>、<code>has_more</code>。</p>
|
||
<h3>0.5 WebSocket 建连、登录和恢复顺序</h3>
|
||
<h4>0.5.1 现有服务端可直接执行的冷启动流程</h4>
|
||
<pre><code class="language-text">
|
||
读取客户端预置的 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 增量拉取
|
||
-> 进入可发送状态
|
||
</code></pre>
|
||
<p>当前服务端已提供 <code>/v1/im/tenants/resolve</code>,用于进入企业前校验企业码并返回公开配置;但它不能代替 WebSocket 第一帧 <code>{hi}</code>。即使 HTTP 解析成功,WebSocket Upgrade 返回 <code>101 Switching Protocols</code> 后仍必须先发送下面的 <code>{hi}</code> JSON 文本帧,并使用同一个 <code>tenant_code</code> 绑定当前 Session。</p>
|
||
<p><code>hi</code> 请求:</p>
|
||
<pre><code class="language-json">
|
||
{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","dev":"device-id","lang":"zh-CN","tenant":"acme"}}
|
||
</code></pre>
|
||
<p><code>login</code> 请求使用 <code>basic</code> 或 <code>token</code>:</p>
|
||
<pre><code class="language-json">
|
||
{"login":{"id":"login-1","scheme":"basic","secret":"base64(username:password)"}}
|
||
</code></pre>
|
||
<pre><code class="language-json">
|
||
{"login":{"id":"login-2","scheme":"token","secret":"<上次登录响应原样返回的 token 字符串>"}}
|
||
</code></pre>
|
||
<p><code>basic.secret</code> 是 <code>base64(username + ":" + password)</code>。Token 登录必须把上次 <code>params.token</code> 的 JSON 字符串原样填入 <code>secret</code>,不能再次 Base64 编码。登录成功 <code>{ctrl}</code> 的 <code>params</code> 至少包含 <code>user</code>、<code>authlvl</code>、<code>token</code>、<code>expires</code>。产品 HTTP 也使用同一个 <code>params.token</code>:<code>Authorization: Bearer <params.token></code>,客户端不要解码或二次编码。Token 对客户端是不可解析的 opaque 字符串,只能存入系统安全存储。<code>id</code> 只用于匹配本次 Session 的命令响应,不是业务幂等键。</p>
|
||
<p><code>hi</code> 成功示例:</p>
|
||
<pre><code class="language-json">
|
||
{
|
||
"ctrl": {
|
||
"id": "hi-1",
|
||
"code": 201,
|
||
"text": "created",
|
||
"params": {
|
||
"ver": "0.25",
|
||
"tenant": {"code": "acme", "name": "示例企业"},
|
||
"maxFileUploadSize": 104857600,
|
||
"callTimeout": 30,
|
||
"iceServers": []
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>客户端只使用实际响应中存在的限制字段;未知字段忽略。<code>hi</code> 返回非 2xx 时必须关闭连接,不得继续发送 <code>login</code>。</p>
|
||
<p>登录成功后发送:</p>
|
||
<pre><code class="language-json">
|
||
{"sub":{"id":"sub-me-1","topic":"me","get":{"what":"desc sub tags cred"}}}
|
||
</code></pre>
|
||
<p>客户端按相同 <code>id</code> 接收 <code>{ctrl}</code> 和一个或多个 <code>{meta}</code>,从 <code>meta.desc</code> 取得本人资料和读写游标,从 <code>meta.sub</code> 取得会话 Topic、访问权限、<code>seq/read/recv</code> 与会话摘要。不要依赖 <code>{ctrl}</code>、<code>{meta}</code> 的固定先后顺序。</p>
|
||
<p>随后建立查找 Topic:</p>
|
||
<pre><code class="language-json">
|
||
{"sub":{"id":"sub-fnd-1","topic":"fnd"}}
|
||
</code></pre>
|
||
<p>进入具体会话时订阅并增量拉取,<code>since</code> 使用客户端已完整持久化的最后 <code>seq + 1</code>:</p>
|
||
<pre><code class="language-json">
|
||
{"sub":{"id":"sub-topic-1","topic":"grp...","get":{"what":"desc sub data del","data":{"since":125,"limit":50},"del":{"since":1,"limit":50}}}}
|
||
</code></pre>
|
||
<p>订阅成功后才允许 <code>{pub}</code>、<code>note read/recv</code> 等会话操作。断线恢复必须重新执行 <code>WebSocket Upgrade -> hi -> token login -> sub me -> 恢复 Topic</code>,同一连接内不得跳过 <code>hi</code> 或切换企业码。</p>
|
||
<h4>0.5.2 Long Polling 兼容流程:先取得的是 sid</h4>
|
||
<p>如果客户端无法使用 WebSocket,可以使用现有 <code>/v0/channels/lp</code>。该模式确实需要“先请求取得一个值”,但取得的是短期 Session ID <code>sid</code>,API Key 仍然必须由客户端预先持有。</p>
|
||
<p>第一步,创建 Long Polling Session:</p>
|
||
<pre><code class="language-http">
|
||
POST /v0/channels/lp HTTP/1.1
|
||
Host: im.example.com
|
||
X-Tinode-APIKey: <预置的普通 API Key>
|
||
Content-Length: 0
|
||
</code></pre>
|
||
<p>服务端返回 HTTP 201:</p>
|
||
<pre><code class="language-json">
|
||
{"ctrl":{"code":201,"text":"created","params":{"sid":"session-id"},"ts":"2026-08-11T08:30:15.123Z"}}
|
||
</code></pre>
|
||
<p>第二步开始,所有 Long Polling 请求都同时携带 API Key 和 <code>sid</code>。发送 <code>hi</code>:</p>
|
||
<pre><code class="language-http">
|
||
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"}}
|
||
</code></pre>
|
||
<p>POST 只负责把命令送入 Session。客户端再保持一个不带 Body 的 GET 来接收下一条 <code>{ctrl}</code>、<code>{meta}</code>、<code>{data}</code>、<code>{pres}</code> 或 <code>{info}</code>:</p>
|
||
<pre><code class="language-http">
|
||
GET /v0/channels/lp?sid=session-id HTTP/1.1
|
||
X-Tinode-APIKey: <预置的普通 API Key>
|
||
</code></pre>
|
||
<p>收到 <code>hi</code> 成功后,以相同方式 POST <code>{login}</code>,再 GET 登录结果;登录成功后依次 POST <code>{sub me}</code>、<code>{sub fnd}</code> 和会话订阅,并持续发起下一次 GET 等待下行消息。</p>
|
||
<p><code>sid</code> 无效或过期时服务端返回 HTTP 403 <code>invalid or expired session</code>,客户端必须重新创建 Long Polling Session 并从 <code>hi</code> 开始,不能只重发 <code>login</code>。Long Polling 是兼容方案,移动端和 Web 端正常网络条件下优先使用 WebSocket。</p>
|
||
<h4>0.5.3 企业解析与产品流程</h4>
|
||
<p>第 1.1 节的 <code>POST /v1/im/tenants/resolve</code> 已实现,推荐产品入口流程为:</p>
|
||
<pre><code class="language-text">
|
||
读取预置 API Key
|
||
-> POST /v1/im/tenants/resolve 校验企业码并取得服务地址/品牌配置
|
||
-> 带同一 API Key 建立返回地址的 WebSocket
|
||
-> hi 再次绑定并校验 tenant_code
|
||
-> login -> sub me -> 恢复会话
|
||
</code></pre>
|
||
<p>企业解析接口只改善入口、服务发现和品牌配置,不能代替 <code>{hi}</code> 的租户绑定。即使 HTTP 解析成功,WebSocket 第一帧仍必须带相同 <code>tenant_code</code>。如果解析响应中的 <code>endpoints</code> 不符合当前发布环境,客户端应优先使用内置的公网环境配置,并上报脱敏诊断。</p>
|
||
<h3>0.6 Topic 与产品消息</h3>
|
||
<p>核心命令:<code>sub</code> 订阅、<code>pub</code> 发送、<code>get</code> 拉取、<code>set</code> 更新 Topic、<code>leave</code> 离开、<code>del</code> 删除、<code>note</code> 发送回执/临时状态。发送成功为 <code>{ctrl code:202, params:{seq}}</code>,服务端下行消息为 <code>{data}</code>。</p>
|
||
<p>所有产品消息在 <code>pub.head</code> 中携带:</p>
|
||
<pre><code class="language-json">
|
||
{
|
||
"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":"你好"}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p><code>x-im-client-mid</code> 由客户端生成 ULID/UUID,作用域为“租户 + 发送人”,断线重试必须复用;服务端目标实现应返回第一次写入的同一 <code>seq</code>。未知 <code>x-im-type</code> 显示“暂不支持的消息”,不得崩溃或删除消息。</p>
|
||
<p>消息类型首版固定为:<code>text</code>、<code>image</code>、<code>video</code>、<code>voice</code>、<code>file</code>、<code>contact_card</code>、<code>link</code>、<code>forward_bundle</code>、<code>call</code>、<code>system_event</code>。自定义表情启用后追加 <code>sticker</code>。媒体内容只保存 <code>file_id</code>、文件元数据和缩略图 ID,不保存可长期绕过鉴权的公共 URL。</p>
|
||
<h3>0.7 HTTP 幂等、重试和安全</h3>
|
||
<ol>
|
||
<li>创建、提交、同意、拒绝、删除、转让、收藏、转发等写请求必须携带 <code>Idempotency-Key</code>。</li>
|
||
<li>GET、断线同步和同幂等键写请求可自动重试;密码、验证码和风控确认只按接口返回的 <code>retry_after_seconds</code> 重试。</li>
|
||
<li><code>401 TOKEN_EXPIRED</code> 只允许一次静默刷新;刷新失败必须退出登录。<code>403</code> 不重试,<code>429</code> 按服务端退避。</li>
|
||
<li>服务端必须在最终写入前再次校验好友关系、群角色、禁言、消息归属、文件 ACL、租户配额和风控,不能信任前端按钮状态。</li>
|
||
<li>普通用户禁止向系统 Topic 发布;系统通知必须由服务端受控身份生成。</li>
|
||
</ol>
|
||
<h2>1. 账号注册、登录与身份安全</h2>
|
||
<h3>1.1 企业码登录.png</h3>
|
||
<figure><img src="images/企业码登录.png" alt="企业码登录"><figcaption>企业码登录</figcaption></figure>
|
||
<p><strong>状态:企业解析接口与 WebSocket 租户握手均已实现。</strong></p>
|
||
<p><strong>当前系统的执行方式:</strong></p>
|
||
<ul>
|
||
<li>前端动作:输入企业码并点击“进入企业”。只去掉首尾空格并校验非空、最长 64 字符;当前服务端没有自动转小写,客户端不得擅自改变用户输入的大小写。</li>
|
||
<li>客户端从构建/环境配置读取 <code>host</code> 和普通 API Key,可先调用 <code>POST /v1/im/tenants/resolve</code> 获取企业公开信息,再使用 <code>wss://{host}/{api_path}v0/channels?apikey={api_key}</code> 建立 WebSocket。</li>
|
||
<li>Upgrade 成功后第一帧发送 <code>{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"用户输入的企业码"}}</code>。</li>
|
||
<li>服务端通过 <code>im_tenant.code</code> 查询企业;成功返回 <code>{ctrl code:201}</code> 和 <code>params.tenant{code,name}</code>,同时固定 Session 的内部 <code>tenant_id</code>。客户端保存服务端返回的标准 <code>tenant.code/name</code>,再进入 1.2 登录。</li>
|
||
<li>当前失败表现:企业码缺失/超过 64 字符返回 400,企业不存在按存储错误返回,企业停用返回 403。当前还没有统一 <code>TENANT_UNAVAILABLE</code> 产品错误码。</li>
|
||
</ul>
|
||
<p><strong>企业解析接口:</strong></p>
|
||
<ul>
|
||
<li><code>POST /v1/im/tenants/resolve</code> 未登录可调用,但必须带 <code>X-Tinode-APIKey</code> 和 <code>X-IM-Schema: 1</code>;Header 传 <code>X-Tenant-Code: acme</code>,Body 可传 <code>{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}</code>。</li>
|
||
<li>成功数据:<code>tenant{code,name,tenant_desc,branding}</code>、<code>endpoints{websocket,api,file}</code>、<code>auth_methods</code>、<code>password_policy</code>、<code>app_update</code>。不得返回 <code>tenant_id</code> 或 <code>tenant_ticket</code>。</li>
|
||
<li>后续:保存公开企业信息,连接响应给出的 WebSocket 地址,仍然发送 0.5 节的 <code>hi(tenant)</code> 做最终租户绑定;<code>hi</code> 成功后才进入 1.2。</li>
|
||
<li>目标失败协议:企业不存在、停用或不允许登录统一返回 <code>TENANT_UNAVAILABLE</code>;客户端不得区分不存在和停用。网络失败保留输入并允许重试。</li>
|
||
</ul>
|
||
<p>成功响应示例:</p>
|
||
<pre><code class="language-json">
|
||
{
|
||
"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"
|
||
}
|
||
</code></pre>
|
||
<h3>1.2 登录页面.png</h3>
|
||
<figure><img src="images/登录页面.png" alt="登录页面"><figcaption>登录页面</figcaption></figure>
|
||
<p><strong>状态:基本账号密码登录已实现;WS Token 可复用于产品 HTTP。</strong></p>
|
||
<ul>
|
||
<li>前端动作:账号、密码非空且已勾选协议后启用登录;密码仅驻留当前页面内存。</li>
|
||
<li>协议:已完成 <code>hi</code> 的 WebSocket 发送 <code>{login scheme:"basic"}</code>,<code>secret=base64(username:password)</code>;不得先登录再传企业码。</li>
|
||
<li>成功:读取 <code>{ctrl params.user/token/expires/authlvl}</code>,Token 存 Keychain/Keystore;立即 <code>sub me</code>,再加载会话。前端不解码 Token,也不再次 Base64 编码。</li>
|
||
<li>产品 HTTP:使用同一个 <code>params.token</code> 作为 <code>Authorization: Bearer <params.token></code>;所有 <code>/v1/im/*</code> HTTP 请求仍必须带 <code>X-Tinode-APIKey</code>、<code>X-IM-Schema: 1</code>,写请求再带 <code>Idempotency-Key</code>。</li>
|
||
<li>待改造:后续可演进为独立短期 access token 与可轮换 refresh token;当前版本先复用 Tinode 登录 Token,并已绑定 <code>tenant_id/user_id/auth_version</code>。修改密码、封禁账号后旧 Token 应失效。</li>
|
||
<li>错误:<code>AUTH_FAILED</code> 统一显示账号或密码错误;<code>TENANT_MISMATCH</code> 清 Token 并回企业码页;<code>ACCOUNT_DISABLED</code>、<code>REALNAME_REQUIRED</code>、<code>YOUTH_RESTRICTED</code> 按专用页面处理。</li>
|
||
</ul>
|
||
<p>登录成功示例:</p>
|
||
<pre><code class="language-json">
|
||
{
|
||
"ctrl": {
|
||
"id": "login-1",
|
||
"code": 200,
|
||
"text": "ok",
|
||
"params": {
|
||
"user": "usr...",
|
||
"authlvl": "auth",
|
||
"token": "base64-opaque-token",
|
||
"expires": "2026-08-11T10:30:15.123Z"
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>失败仍是同一 <code>{ctrl}</code> 结构,使用 <code>code</code> 和改造后的稳定 <code>params.reason</code> 分支;客户端不得用 <code>text</code> 做程序判断。</p>
|
||
<h3>1.3 登录页面-密码错误.png</h3>
|
||
<figure><img src="images/登录页面-密码错误.png" alt="登录页面-密码错误"><figcaption>登录页面-密码错误</figcaption></figure>
|
||
<p><strong>状态:已实现基础错误,错误码和限流需改造。</strong></p>
|
||
<ul>
|
||
<li>触发:<code>login</code> 返回 <code>{ctrl code:401}</code> 或产品错误 <code>AUTH_FAILED</code>。</li>
|
||
<li>前端:保留账号、清空密码、聚焦密码框;统一显示“账号或密码错误”,不得显示账号是否存在。</li>
|
||
<li>服务端:按 <code>tenant + account + device/IP</code> 限流,连续失败返回 <code>AUTH_RATE_LIMITED</code> 和 <code>retry_after_seconds</code>;日志仅记录账号哈希和 Request ID。</li>
|
||
<li>验收:同一不存在账号与错误密码在响应码、耗时区间、前端文案上保持一致。</li>
|
||
</ul>
|
||
<h3>1.4 注册页面.png</h3>
|
||
<figure><img src="images/注册页面.png" alt="注册页面"><figcaption>注册页面</figcaption></figure>
|
||
<p><strong>状态:Tinode <code>{acc}</code> 基础注册和验证码预校验已实现;短信供应商接入前固定测试码为 <code>123456</code>。</strong></p>
|
||
<ul>
|
||
<li>获取验证码:调用 <code>POST /v1/im/auth/verification-codes</code>,必须带 <code>X-Tinode-APIKey</code>、<code>X-Tenant-Code</code>、<code>X-IM-Schema: 1</code>,Body <code>{"scene":"register","phone":"+86138..."}</code>;成功返回 <code>challenge_id</code>、<code>expires_in_seconds</code>、<code>resend_after_seconds</code>。</li>
|
||
<li>提交注册:前端校验密码一致和协议后,先调用 <code>POST /v1/im/auth/verification-codes/{challenge_id}/verify</code>,Body <code>{"code":"123456"}</code>,获得一次性 <code>verification_token</code>。</li>
|
||
<li>WebSocket:已完成同一企业 <code>hi</code> 后发送 <code>{acc}</code>。把一次性验证结果放在 <code>{acc}.cred[0].resp</code>,它必须是 HTTP verify 返回的 <code>verification_token</code>,不是短信验证码 <code>123456</code>。不得写入 <code>desc.private</code>,也不得在 <code>{acc}</code> 顶层传 <code>tags</code>。服务端会根据已校验的 <code>cred</code> 建立手机号凭证;客户端主动设置 <code>tags:["tel:..."]</code> 会被 Tinode 权限策略拒绝并返回 <code>403 params.what=tags</code>。</li>
|
||
<li>手机号格式:HTTP 发码、验证码校验、<code>{acc}.secret</code> 中的 username、<code>cred.val</code> 必须使用同一个规范化号码,推荐 E.164 格式如 <code>+86138...</code>;不要一处使用 <code>+86138...</code>、另一处使用 <code>86138...</code>。</li>
|
||
</ul>
|
||
<pre><code class="language-json">
|
||
{"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":"用户昵称"}}}}
|
||
</code></pre>
|
||
<ul>
|
||
<li>成功:返回新 <code>user</code> 和登录 Token;客户端保存 <code>params.token</code>,随后执行 <code>sub me</code> 和会话恢复。创建用户前原子校验 <code>im_tenant_config.max_users</code>,超限返回 <code>TENANT_USER_QUOTA_EXCEEDED</code>。</li>
|
||
<li>安全:手机号在租户内唯一;验证码只保存摘要、单次消费;密码按服务端策略校验并使用强哈希保存。</li>
|
||
</ul>
|
||
<h3>1.5 注册页面-倒计时.png</h3>
|
||
<figure><img src="images/注册页面-倒计时.png" alt="注册页面-倒计时"><figcaption>注册页面-倒计时</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>倒计时以 1.4 返回的 <code>resend_after_seconds</code> 为准,并结合 <code>server_time</code> 计算,不能只依赖本地固定 60 秒。</li>
|
||
<li>倒计时内前端禁用按钮;即使绕过按钮重复调用,服务端返回 <code>VERIFICATION_RATE_LIMITED</code> 和剩余秒数。</li>
|
||
<li>App 重进页面可使用内存/安全临时存储中的 <code>challenge_id</code> 恢复;超过 <code>expires_in_seconds</code> 必须重新发送,旧验证码失效。</li>
|
||
</ul>
|
||
<h3>1.6 忘记密码.png</h3>
|
||
<figure><img src="images/忘记密码.png" alt="忘记密码"><figcaption>忘记密码</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>发码:<code>POST /v1/im/auth/verification-codes</code>,Body <code>{"scene":"reset_password","phone":"..."}</code>。无论账号是否存在都返回相同受理响应。</li>
|
||
<li>校验:<code>POST /v1/im/auth/password-reset/challenges/{challenge_id}/verify</code>,Body <code>{"code":"123456"}</code>,成功返回短时、单次使用的 <code>reset_token</code>。</li>
|
||
<li>重置:<code>POST /v1/im/auth/password-reset/confirm</code>,Body <code>{"reset_token":"...","new_password":"..."}</code>,带幂等键。</li>
|
||
<li>成功:服务端更新密码、递增 <code>auth_version</code>、撤销该用户全部 access/refresh token 和在线 Session;客户端清理本租户凭证并返回登录页。</li>
|
||
<li>失败:<code>PASSWORD_POLICY_VIOLATION</code> 返回规则明细;验证码错误不返回账号存在性,达到上限后冻结 challenge。</li>
|
||
</ul>
|
||
<h3>1.7 身份验证(安卓).png</h3>
|
||
<figure><img src="images/身份验证(安卓).png" alt="身份验证(安卓)"><figcaption>身份验证(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>敏感操作先调用 <code>POST /v1/im/security/challenges</code>,Body <code>{"scene":"close_youth_mode","channel":"sms"}</code>;手机号由登录身份读取,客户端不提交目标手机号。</li>
|
||
<li>发码/校验:服务端返回 <code>challenge_id</code> 和脱敏手机号;提交 <code>POST /v1/im/security/challenges/{id}/verify</code>,Body <code>{"code":"..."}</code>。</li>
|
||
<li>成功返回 <code>action_token</code>、<code>action</code>、<code>expires_at</code>;后续敏感接口以 <code>X-Action-Token</code> 携带,Token 只能用于指定动作且单次消费。</li>
|
||
<li>错误:<code>CHALLENGE_EXPIRED</code>、<code>CODE_INVALID</code>、<code>TOO_MANY_ATTEMPTS</code>;前端不得把短信验证码或 action token 落普通存储。</li>
|
||
</ul>
|
||
<h3>1.8 实名认证.png</h3>
|
||
<figure><img src="images/实名认证.png" alt="实名认证"><figcaption>实名认证</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>查询:<code>GET /v1/im/security/realname</code> 返回 <code>status=unverified|pending|verified|rejected</code>、脱敏姓名/证件号和 <code>can_submit</code>。</li>
|
||
<li>提交:<code>POST /v1/im/security/realname/submissions</code>,Body <code>{"name":"张三","id_type":"cn_id","id_number":"..."}</code>,必须带幂等键。</li>
|
||
<li>成功:返回 <code>submission_id</code>、<code>status:"pending"</code>;前端禁用重复提交,通过 <code>GET /v1/im/security/realname</code> 或 <code>security.realname.updated</code> 系统事件更新状态。</li>
|
||
<li>安全:证件号传输和存储加密、展示脱敏;普通日志、搜索索引和消息中禁止出现原值;重复身份由租户策略决定并返回稳定错误。</li>
|
||
</ul>
|
||
<h3>1.9 实名认证-已实名.png</h3>
|
||
<figure><img src="images/实名认证-已实名.png" alt="实名认证-已实名"><figcaption>实名认证-已实名</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>页面只调用 <code>GET /v1/im/security/realname</code>。<code>verified</code> 时返回如 <code>masked_name:"张*"</code>、<code>masked_id_number:"110***********1234"</code>、<code>verified_at</code>。</li>
|
||
<li>前端只读展示,不缓存原始证件信息,不提供直接修改接口。</li>
|
||
<li>更正信息使用独立申诉 <code>POST /v1/im/security/realname/appeals</code>,需二次身份验证;不复用本页保存按钮。</li>
|
||
</ul>
|
||
<h3>1.10 实名认证(安卓).png</h3>
|
||
<figure><img src="images/实名认证(安卓).png" alt="实名认证(安卓)"><figcaption>实名认证(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>登录后 <code>GET /v1/im/bootstrap</code> 返回 <code>security.realname{status,required,allow_skip,remind_after}</code>;只有 <code>required=true</code> 且未认证时显示弹窗。</li>
|
||
<li>点击“去认证”进入 1.8;点击稍后仅在 <code>allow_skip=true</code> 时本地关闭,并按 <code>remind_after</code> 控制再次提示。</li>
|
||
<li>服务端对必须实名的受限写操作仍返回 <code>REALNAME_REQUIRED</code>,前端弹窗不能作为授权依据。</li>
|
||
</ul>
|
||
<h3>1.11 设置独立密码(安卓).png</h3>
|
||
<figure><img src="images/设置独立密码(安卓).png" alt="设置独立密码(安卓)"><figcaption>设置独立密码(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>设置:<code>POST /v1/im/settings/independent-password</code>,Body <code>{"password":"1234","purpose":"youth_mode","action_token":"..."}</code>;服务端只存强哈希,不返回密码。</li>
|
||
<li>验证:<code>POST /v1/im/settings/independent-password/verify</code> 返回短时 <code>action_token</code>,用于关闭青少年模式或修改密码。</li>
|
||
<li>规则:四位数字、禁止明显连续/重复组合;连续错误返回 <code>TOO_MANY_ATTEMPTS</code> 和 <code>retry_after_seconds</code>。前端输入框禁截屏/自动填充,离开页面立即清空。</li>
|
||
<li>忘记密码:跳转 1.7 的短信身份验证,验证后重置,不提供明文找回。</li>
|
||
</ul>
|
||
<h2>2. 消息首页、全局导航与系统通知</h2>
|
||
<h3>2.1 消息.png</h3>
|
||
<figure><img src="images/消息.png" alt="消息"><figcaption>消息</figcaption></figure>
|
||
<p><strong>状态:会话订阅已实现;产品摘要和系统入口需改造。</strong></p>
|
||
<ul>
|
||
<li>登录后发送 <code>sub me</code>,再通过 <code>get</code> 拉取 <code>sub</code> 元数据;每项使用 <code>topic</code>、<code>public</code>、<code>private</code>、<code>updated</code>、<code>seq/read/recv</code> 计算名称、摘要和未读数。</li>
|
||
<li>消息增量通过各 Topic 的 <code>{data}</code> 更新;未读数为 <code>max(seq-read,0)</code>,回执以服务端游标为准,不能只用本地计数。</li>
|
||
<li><code>GET /v1/im/bootstrap</code> 返回固定入口、功能开关、权限和系统 Topic;搜索调用 <code>GET /v1/im/search/conversations?q=&cursor=</code>。</li>
|
||
<li>本地缓存按 <code>tenant_code + user_id</code> 隔离。切租户、退出或账号变化时不得复用另一身份的会话摘要。</li>
|
||
</ul>
|
||
<h3>2.2 消息-无网络状态.png</h3>
|
||
<figure><img src="images/消息-无网络状态.png" alt="消息-无网络状态"><figcaption>消息-无网络状态</figcaption></figure>
|
||
<p><strong>状态:需改造客户端恢复流程和消息幂等。</strong></p>
|
||
<ul>
|
||
<li>纯网络状态由客户端监听;断网时展示缓存并禁止需要在线确认的写操作,未发送消息进入本地 <code>pending</code> 队列。</li>
|
||
<li>使用指数退避加随机抖动重连;恢复后严格执行 <code>hi -> token login -> sub me -> 恢复 Topic -> get data</code>。</li>
|
||
<li>每个 Topic 按最后持久化的 <code>seq/read/recv</code> 增量拉取。重发消息复用 <code>x-im-client-mid</code>,服务端需实现去重并返回原 <code>seq</code>。</li>
|
||
<li>Token 失效转登录页;租户停用返回企业不可用页;禁止无限快速重连。</li>
|
||
</ul>
|
||
<h3>2.3 消息-更多.png</h3>
|
||
<figure><img src="images/消息-更多.png" alt="消息-更多"><figcaption>消息-更多</figcaption></figure>
|
||
<p><strong>状态:菜单为纯客户端;权限数据新增。</strong></p>
|
||
<ul>
|
||
<li>菜单打开/关闭不调用服务端。菜单项来自登录后 <code>GET /v1/im/bootstrap</code> 的 <code>features</code> 与 <code>permissions</code>。</li>
|
||
<li><code>create_group=false</code> 时隐藏“创建群聊”;<code>user_search=false</code> 时隐藏“添加好友”;扫码权限只影响实际扫码,不影响服务端授权。</li>
|
||
<li>各入口协议分别见 5.3、6.1、10.1;服务端在实际写接口再次鉴权,不能只依赖隐藏按钮。</li>
|
||
</ul>
|
||
<h3>2.4 消息页面—长按弹出.png</h3>
|
||
<figure><img src="images/消息页面—长按弹出.png" alt="消息页面—长按弹出"><figcaption>消息页面—长按弹出</figcaption></figure>
|
||
<p><strong>状态:基础消息操作部分已实现;产品权限矩阵需改造。</strong></p>
|
||
<ul>
|
||
<li>长按本身不请求。客户端根据消息的 <code>type/from/seq/created_at/status</code>、当前 Topic 权限和服务端下发的 <code>message_policy</code> 预裁剪菜单。</li>
|
||
<li>引用/复制为本地动作;收藏见 4.16;转发见 4.17~4.20;删除使用 Tinode <code>{del what:"msg",topic,delseq}</code>;撤回需新增 <code>POST /v1/im/topics/{topic}/messages/{seq}/recall</code>。</li>
|
||
<li>编辑使用 <code>POST /v1/im/topics/{topic}/messages/{seq}/revisions</code>,Body <code>{"content":...,"client_mid":"..."}</code>;仅文本且在时限内允许。</li>
|
||
<li>群回执调用 <code>GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor=</code>。所有最终操作由服务端验证消息归属、时限、群角色和租户。</li>
|
||
</ul>
|
||
<h3>2.5 系统消息.png</h3>
|
||
<figure><img src="images/系统消息.png" alt="系统消息"><figcaption>系统消息</figcaption></figure>
|
||
<p><strong>状态:需改造。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/system-notifications?cursor=</code> 返回 <code>id,event_type,title,summary,created_at,read_at,action</code>;新通知通过服务端专用系统 Topic 的 <code>system_event</code> 消息推送。</li>
|
||
<li>新设备登录事件内容为 <code>device_name,platform,ip_masked,login_at,security_action</code>;IP 只下发必要脱敏值。</li>
|
||
<li>已读:<code>POST /v1/im/system-notifications/{id}/read</code>,带幂等键;跳转目标只接受服务端白名单 <code>action.type</code> 和内部参数。</li>
|
||
<li>当前普通认证用户可向 <code>sys</code> Topic 发布的路径必须封禁。客户端不得渲染普通用户伪造的系统卡片为可信通知。</li>
|
||
</ul>
|
||
<h2>3. 单聊会话、会话建立与消息状态</h2>
|
||
<h3>3.1 个人对话框(对方的状态).png</h3>
|
||
<figure><img src="images/个人对话框(对方的状态).png" alt="个人对话框(对方的状态)"><figcaption>个人对话框(对方的状态)</figcaption></figure>
|
||
<p><strong>状态:需改造好友关系状态。</strong></p>
|
||
<ul>
|
||
<li>进入会话先 <code>GET /v1/im/conversations/{topic}/context</code>,返回 <code>relationship=pending_inbound</code>、<code>can_send=false</code>、对方公开资料和申请 ID。</li>
|
||
<li>历史申请提示由 <code>relationship_event</code> 系统消息渲染;普通消息仍通过 Topic <code>{data}</code> 下行。</li>
|
||
<li>输入区锁定;“通过/拒绝”调用 5.2 的好友申请接口。前端不可通过直接 <code>pub</code> 绕过,服务端发布路径必须校验好友关系。</li>
|
||
</ul>
|
||
<h3>3.2 个人对话框(对方的状态)(1).png</h3>
|
||
<p>!<a href="images/个人对话框(对方的状态)(1">个人对话框(对方的状态)(1)</a>.png)</p>
|
||
<p><strong>状态:在线状态已具备基础能力;关系文案新增。</strong></p>
|
||
<ul>
|
||
<li>订阅 P2P Topic 后用 <code>{pres}</code> 更新在线/离线,<code>last_seen</code> 仅在对方隐私允许时展示。</li>
|
||
<li><code>GET /v1/im/conversations/{topic}/context</code> 返回 <code>display_name,avatar_file_id,online_visibility,relationship</code>。</li>
|
||
<li>好友关系变化由服务端生成 <code>system_event{event_type:"friend_relationship_changed"}</code>,客户端不得根据本地动作自行永久插入系统记录。</li>
|
||
</ul>
|
||
<h3>3.3 个人对话框(我的状态).png</h3>
|
||
<figure><img src="images/个人对话框(我的状态).png" alt="个人对话框(我的状态)"><figcaption>个人对话框(我的状态)</figcaption></figure>
|
||
<p><strong>状态:新增好友申请状态。</strong></p>
|
||
<ul>
|
||
<li>发起申请使用 <code>POST /v1/im/friend-requests</code>,Body <code>{"target_user_id":"usr...","message":"我是...","source":"search"}</code>。</li>
|
||
<li>返回 <code>request_id,status:"pending",created_at</code>;页面以该服务端状态显示“已发送”,重复点击用同一幂等键返回原申请。</li>
|
||
<li>申请待处理时 <code>context.relationship=pending_outbound</code>、<code>can_send=false</code>,禁止重复创建和普通消息发送。</li>
|
||
</ul>
|
||
<h3>3.4 个人对话框(我的状态)(1).png</h3>
|
||
<p>!<a href="images/个人对话框(我的状态)(1">个人对话框(我的状态)(1)</a>.png)</p>
|
||
<p><strong>状态:发送和读回执已实现基础能力;送达状态映射需统一。</strong></p>
|
||
<ul>
|
||
<li>本地生成 <code>client_mid</code> 后显示 <code>sending</code>;<code>pub</code> 收到 <code>202 + seq</code> 更新为 <code>sent</code>;失败保留同一 <code>client_mid</code> 重试。</li>
|
||
<li>对方 <code>note what:"recv"</code> 更新 <code>delivered</code>,<code>note what:"read"</code> 更新 <code>read</code>;多设备取服务端最大游标。</li>
|
||
<li>状态只附着在气泡元数据,不修改消息正文。服务端拒绝时映射为 <code>failed(reason)</code>,不得伪装成已发送。</li>
|
||
</ul>
|
||
<h3>3.5 开启沟通页面.png</h3>
|
||
<figure><img src="images/开启沟通页面.png" alt="开启沟通页面"><figcaption>开启沟通页面</figcaption></figure>
|
||
<p><strong>状态:新增好友审批与 Topic 联动。</strong></p>
|
||
<ul>
|
||
<li>好友申请通过后,服务端在同一事务内建立双方关系和 P2P 订阅,并生成 <code>friend_relationship_changed:accepted</code> 系统事件。</li>
|
||
<li>客户端收到事件或重新查询 <code>context.relationship=friend</code> 后解锁输入区;不得仅因本地“通过”按钮成功动画解锁。</li>
|
||
<li>首次进入订阅 P2P Topic,再按 <code>since</code> 拉取申请上下文和正常消息。</li>
|
||
</ul>
|
||
<h3>3.6 双方沟通状态.png</h3>
|
||
<figure><img src="images/双方沟通状态.png" alt="双方沟通状态"><figcaption>双方沟通状态</figcaption></figure>
|
||
<p><strong>状态:上传基础已实现;媒体消息 Schema、ACL 和恢复需改造。</strong></p>
|
||
<ul>
|
||
<li>选择媒体后本地生成 <code>client_mid</code> 和稳定占位;调用文件上传,得到 <code>file_id</code> 后再 <code>pub x-im-type=image|video|voice</code>。</li>
|
||
<li><code>content</code> 至少含 <code>file_id,name,size,mime,width,height,duration_ms,thumbnail_file_id</code> 中适用字段;上传进度是客户端任务状态,不写成聊天消息。</li>
|
||
<li>服务端发布成功后关联文件与 Topic/消息;文件下载必须校验消息可见性。上传失败只重试上传,成功但 <code>pub</code> 失败时复用已上传 <code>file_id</code> 和 <code>client_mid</code>。</li>
|
||
</ul>
|
||
<h3>3.7 个人对话—拉黑状态/已读状态.png</h3>
|
||
<figure><img src="images/个人对话—拉黑状态/已读状态.png" alt="个人对话—拉黑状态/已读状态"><figcaption>个人对话—拉黑状态/已读状态</figcaption></figure>
|
||
<p><strong>状态:黑名单产品规则新增。</strong></p>
|
||
<ul>
|
||
<li>拉黑后 <code>context.relationship=blocked_by_me|blocked_by_peer</code>、<code>can_send=false</code>;服务端 <code>pub</code> 返回统一 <code>MESSAGE_NOT_ALLOWED</code>,不得向发送者泄露具体是对方拉黑。</li>
|
||
<li>失败消息只保存在发送端本地并显示失败,不下发给对方、不写入正常消息表。</li>
|
||
<li>已读仍按历史最大 <code>read</code> 渲染;拉黑不能回滚既有回执。解除拉黑见 5.10、5.12。</li>
|
||
</ul>
|
||
<h3>3.8 个人对话—删除状态.png</h3>
|
||
<figure><img src="images/个人对话—删除状态.png" alt="个人对话—删除状态"><figcaption>个人对话—删除状态</figcaption></figure>
|
||
<p><strong>状态:新增好友删除后的发送约束。</strong></p>
|
||
<ul>
|
||
<li>删除好友使用 <code>DELETE /v1/im/friends/{user_id}</code>;成功后双方关系变为 <code>none</code>,服务端推送关系变化事件。</li>
|
||
<li>后续 <code>pub</code> 返回 <code>FRIEND_RELATION_REQUIRED</code>;前端保留失败草稿,提供“重新添加好友”入口,不自动发申请。</li>
|
||
<li>历史聊天是否保留由 Topic 权限决定,删除好友不等于删除双方历史消息。</li>
|
||
</ul>
|
||
<h3>3.9 个人对话-键盘收起状态.png</h3>
|
||
<figure><img src="images/个人对话-键盘收起状态.png" alt="个人对话-键盘收起状态"><figcaption>个人对话-键盘收起状态</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>键盘、表情和附加面板收起不调用服务端;保持列表锚点和未发送草稿。</li>
|
||
<li>草稿默认按 <code>tenant + user + topic</code> 本地加密保存;若实现多端草稿,再单独调用 <code>PUT /v1/im/topics/{topic}/draft</code>,不得混入聊天消息。</li>
|
||
</ul>
|
||
<h3>3.10 个人对话-发各种文件状态.png</h3>
|
||
<figure><img src="images/个人对话-发各种文件状态.png" alt="个人对话-发各种文件状态"><figcaption>个人对话-发各种文件状态</figcaption></figure>
|
||
<p><strong>状态:面板纯客户端;上传需改造。</strong></p>
|
||
<ul>
|
||
<li>打开面板不请求。选择前依据 <code>context.capabilities</code> 判断图片、视频、语音、文件是否允许,调用系统选择器/相机。</li>
|
||
<li>上传前校验 <code>mime,size</code> 和 <code>bootstrap.limits.max_file_size</code>;服务端仍必须重新校验租户配额、文件类型和会话权限。</li>
|
||
<li>具体上传/发消息使用 3.6 协议;相机和麦克风授权见 10.20、10.21。</li>
|
||
</ul>
|
||
<h3>3.11 个人对话-发各种文件状态(1).png</h3>
|
||
<p>!<a href="images/个人对话-发各种文件状态(1">个人对话-发各种文件状态(1)</a>.png)</p>
|
||
<p><strong>状态:需改造产品消息 Schema。</strong></p>
|
||
<ul>
|
||
<li>客户端按 <code>x-im-type</code> 选择文本、名片、文件等卡片,公共字段统一使用 <code>from,topic,seq,ts,head,content</code>。</li>
|
||
<li>每种卡片必须实现未知字段忽略、未知类型占位、发送/失败/回执状态和长按动作。</li>
|
||
<li>重新安装或换设备后仅凭服务端消息即可还原卡片,不能依赖发送端本地路径或临时 URL。</li>
|
||
</ul>
|
||
<h3>3.12 个人对话-发送名片状态.png</h3>
|
||
<figure><img src="images/个人对话-发送名片状态.png" alt="个人对话-发送名片状态"><figcaption>个人对话-发送名片状态</figcaption></figure>
|
||
<p><strong>状态:新增产品消息类型。</strong></p>
|
||
<ul>
|
||
<li>选人:<code>GET /v1/im/friends?query=&cursor=</code>;选择后发送 <code>x-im-type:"contact_card"</code>。</li>
|
||
<li><code>content</code> 为 <code>{"user_id":"usr...","display_name":"张三","avatar_file_id":"...","public_account":"A1024"}</code>;服务端按目标用户当前公开资料生成/校正快照,禁止手机号、证件号等敏感字段。</li>
|
||
<li>点击卡片调用 <code>GET /v1/im/users/{user_id}/profile</code>;用户不可见或已离职时显示失效卡片,不跨租户查询。</li>
|
||
</ul>
|
||
<h2>4. 消息输入、媒体内容与消息操作</h2>
|
||
<h3>4.1 键盘展开.png</h3>
|
||
<figure><img src="images/键盘展开.png" alt="键盘展开"><figcaption>键盘展开</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>键盘展开、列表上推和光标变化不调用服务端;草稿按 <code>tenant + user + topic</code> 保存。</li>
|
||
<li>点击发送时才校验 <code>context.can_send</code> 并执行 <code>pub text</code>;正文去首尾策略、最大长度从 <code>bootstrap.limits.max_text_length</code> 获取,服务端再次校验。</li>
|
||
</ul>
|
||
<h3>4.2 @人员—选择提醒人.png</h3>
|
||
<figure><img src="images/@人员—选择提醒人.png" alt="@人员—选择提醒人"><figcaption>@人员—选择提醒人</figcaption></figure>
|
||
<p><strong>状态:新增群成员查询。</strong></p>
|
||
<ul>
|
||
<li>输入 <code>@</code> 后调用 <code>GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true</code>,返回 <code>user_id,display_name,avatar_file_id,role,mentionable</code>。</li>
|
||
<li><code>@所有人</code> 是虚拟项,只有响应 <code>permissions.can_mention_all=true</code> 时展示,不以特殊用户 ID 表示。</li>
|
||
<li>搜索做 300ms 防抖并取消旧请求;服务端只返回当前租户且仍在群内的成员。</li>
|
||
</ul>
|
||
<h3>4.3 @人员—完成.png</h3>
|
||
<figure><img src="images/@人员—完成.png" alt="@人员—完成"><figcaption>@人员—完成</figcaption></figure>
|
||
<p><strong>状态:纯客户端选择;发送协议需改造。</strong></p>
|
||
<ul>
|
||
<li>完成后在编辑器插入带范围的 Mention 节点,内部保存 <code>user_id</code>,显示昵称只是快照。</li>
|
||
<li>发送时 <code>pub.head.mentions=[{"user_id":"usr...","offset":0,"length":3}]</code>;<code>@所有人</code> 使用 <code>mention_all:true</code>,不得伪造用户 ID。</li>
|
||
<li>服务端验证成员身份和 <code>mention_all</code> 权限;无效成员可删除提醒语义但保留可读文本,越权 <code>@所有人</code> 返回 <code>MENTION_ALL_FORBIDDEN</code>。</li>
|
||
</ul>
|
||
<h3>4.4 @人员状态.png</h3>
|
||
<figure><img src="images/@人员状态.png" alt="@人员状态"><figcaption>@人员状态</figcaption></figure>
|
||
<p><strong>状态:需改造提醒和 Push。</strong></p>
|
||
<ul>
|
||
<li>消息仍通过 <code>{data}</code> 下行,客户端使用结构化 <code>mentions</code> 高亮,禁止通过字符串扫描昵称判断。</li>
|
||
<li>当前用户被提及时会话摘要标记 <code>mentioned=true</code>;读到对应 <code>seq</code> 后随 <code>note read</code> 清除。</li>
|
||
<li>Push 服务按结构化目标生成“有人@你”文案;已退群用户不能收到新提醒,但历史消息仍按快照渲染。</li>
|
||
</ul>
|
||
<h3>4.5 表情包展开(发送按钮默认状态).png</h3>
|
||
<figure><img src="images/表情包展开(发送按钮默认状态).png" alt="表情包展开(发送按钮默认状态)"><figcaption>表情包展开(发送按钮默认状态)</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>面板、分类、最近使用和删除键均本地处理;未选择内容且文本为空时禁用发送。</li>
|
||
<li>Unicode 表情作为普通 <code>text</code> 内容发送。自定义表情资源首版未定义,不得把本地资源路径直接发送。</li>
|
||
</ul>
|
||
<h3>4.6 表情包发送页面.png</h3>
|
||
<figure><img src="images/表情包发送页面.png" alt="表情包发送页面"><figcaption>表情包发送页面</figcaption></figure>
|
||
<p><strong>状态:Unicode 表情使用已实现文本消息;独立表情包新增。</strong></p>
|
||
<ul>
|
||
<li>文本表情按 <code>pub x-im-type:text</code> 发送。独立自定义表情若启用,使用 <code>x-im-type:"sticker"</code>,<code>content={pack_id,sticker_id,file_id,width,height}</code>。</li>
|
||
<li>自定义表情发送前服务端校验表情包属于当前租户且可用;未知表情显示占位,不使用任意外链 URL。</li>
|
||
</ul>
|
||
<h3>4.7 语音—按住说话.png</h3>
|
||
<figure><img src="images/语音—按住说话.png" alt="语音—按住说话"><figcaption>语音—按住说话</figcaption></figure>
|
||
<p><strong>状态:录音纯客户端;上传和语音消息需改造。</strong></p>
|
||
<ul>
|
||
<li>按下前检查麦克风权限和 <code>context.capabilities.voice</code>;权限不足进入 10.21,不发请求。</li>
|
||
<li>松开发送:编码为服务端允许的 MIME,校验最短/最长时长,上传后 <code>pub x-im-type:voice</code>,内容含 <code>file_id,duration_ms,size,mime,waveform</code>。</li>
|
||
<li>上滑取消、过短、系统来电打断时删除临时文件,不调用上传接口。</li>
|
||
</ul>
|
||
<h3>4.8 语音按住说话.png</h3>
|
||
<figure><img src="images/语音按住说话.png" alt="语音按住说话"><figcaption>语音按住说话</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>音量幅度、上滑取消区域和浮层颜色来自本地录音会话,不上传实时音量。</li>
|
||
<li>进入取消区只改变本地状态;最终抬手决定删除或执行 4.7 的上传发送。</li>
|
||
</ul>
|
||
<h3>4.9 内容沟通对话3-语音录音中.png</h3>
|
||
<figure><img src="images/内容沟通对话3-语音录音中.png" alt="内容沟通对话3-语音录音中"><figcaption>内容沟通对话3-语音录音中</figcaption></figure>
|
||
<p><strong>状态:录音纯客户端;临时状态可选改造。</strong></p>
|
||
<ul>
|
||
<li>录音期间客户端锁定其他媒体入口。默认不向群成员广播“正在录音”,以减少隐私暴露和状态风暴。</li>
|
||
<li>若产品确认需要,可发送不落库的 <code>{note what:"kpa",topic}</code>;服务端限频,其他端超时自动消失,不能作为聊天记录。</li>
|
||
</ul>
|
||
<h3>4.10 内容沟通对话3-图片和语音发送中.png</h3>
|
||
<figure><img src="images/内容沟通对话3-图片和语音发送中.png" alt="内容沟通对话3-图片和语音发送中"><figcaption>内容沟通对话3-图片和语音发送中</figcaption></figure>
|
||
<p><strong>状态:客户端任务管理新增;服务端上传需改造。</strong></p>
|
||
<ul>
|
||
<li>每个待发送项保存 <code>client_mid,local_uri,type,upload_id,progress,state</code>;同一文件可分片/断点续传时使用稳定 <code>upload_id</code>。</li>
|
||
<li>上传完成后再发布消息;多任务可并发但必须各自保持原排序号。离开页面不取消,退出账号必须暂停并隔离任务。</li>
|
||
<li>服务端返回 <code>FILE_TOO_LARGE</code>、<code>FILE_TYPE_NOT_ALLOWED</code>、<code>STORAGE_QUOTA_EXCEEDED</code> 时不可自动无限重试。</li>
|
||
</ul>
|
||
<h3>4.11 内容沟通对话3-语音/视频/图片状态.png</h3>
|
||
<figure><img src="images/内容沟通对话3-语音/视频/图片状态.png" alt="内容沟通对话3-语音/视频/图片状态"><figcaption>内容沟通对话3-语音/视频/图片状态</figcaption></figure>
|
||
<p><strong>状态:需改造媒体 Schema 和受权下载。</strong></p>
|
||
<ul>
|
||
<li>语音渲染 <code>duration_ms,waveform</code>,未听状态保存在客户端并可选跨端同步;视频渲染 <code>thumbnail_file_id,duration_ms,width,height</code>;图片渲染缩略图和原图 ID。</li>
|
||
<li>预览前使用认证文件接口获取,服务端校验当前用户仍可访问引用消息;附件撤回或过期返回 <code>FILE_UNAVAILABLE</code>。</li>
|
||
<li>缩略图和原文件分别授权,客户端缓存键必须包含租户与用户。</li>
|
||
</ul>
|
||
<h3>4.12 语音图片发送状态.png</h3>
|
||
<figure><img src="images/语音图片发送状态.png" alt="语音图片发送状态"><figcaption>语音图片发送状态</figcaption></figure>
|
||
<p><strong>状态:需改造。</strong></p>
|
||
<ul>
|
||
<li>图片选择后立即显示本地缩略图,上传成功后只替换资源来源,不改变 <code>client_mid</code> 或列表位置。</li>
|
||
<li>失败展示重试按钮;重试复用同一上传任务和消息幂等 ID。语音播放与图片上传状态相互独立。</li>
|
||
</ul>
|
||
<h3>4.13 上传文件.png</h3>
|
||
<figure><img src="images/上传文件.png" alt="上传文件"><figcaption>上传文件</figcaption></figure>
|
||
<p><strong>状态:纯客户端视觉资源。</strong></p>
|
||
<ul>
|
||
<li>该图片仅定义统一上传图标和无障碍名称“上传文件”,不产生网络请求。</li>
|
||
<li>文件助手、群文件、会话附件、反馈证据实际上传时统一走 0.2 文件接口,并额外提交业务 <code>purpose</code> 以便服务端执行权限和保留期策略。</li>
|
||
</ul>
|
||
<h3>4.14 文件发送状态.png</h3>
|
||
<figure><img src="images/文件发送状态.png" alt="文件发送状态"><figcaption>文件发送状态</figcaption></figure>
|
||
<p><strong>状态:上传已实现基础能力;消息引用和 ACL 需改造。</strong></p>
|
||
<ul>
|
||
<li>上传成功后发送 <code>x-im-type:file</code>,<code>content={file_id,name,size,mime,extension,checksum}</code>;文件名做长度和控制字符清理。</li>
|
||
<li>服务端在消息落库事务中关联文件;未关联的临时上传按 TTL 清理。卡片下载见 7.9~7.11。</li>
|
||
<li>多文件逐个生成 <code>client_mid</code>,某个失败不能改变其他文件状态。</li>
|
||
</ul>
|
||
<h3>4.15 多选-选中对话框.png</h3>
|
||
<figure><img src="images/多选-选中对话框.png" alt="多选-选中对话框"><figcaption>多选-选中对话框</figcaption></figure>
|
||
<p><strong>状态:选择纯客户端;批量动作新增。</strong></p>
|
||
<ul>
|
||
<li>客户端按 <code>(topic,seq)</code> 保存已选项并显示数量;默认最多 100 条,超限在本地阻止。</li>
|
||
<li>转发、收藏、删除前把选中列表提交对应接口;服务端逐条校验仍可见、类型支持和权限,返回 <code>succeeded[]</code> 与 <code>failed[{seq,reason}]</code>。</li>
|
||
<li>不允许只因第一条可操作就假定整批可操作。</li>
|
||
</ul>
|
||
<h3>4.16 多选-选中对话内容-收藏成功.png</h3>
|
||
<figure><img src="images/多选-选中对话内容-收藏成功.png" alt="多选-选中对话内容-收藏成功"><figcaption>多选-选中对话内容-收藏成功</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>POST /v1/im/favorites/batch</code>,Body <code>{"messages":[{"topic":"grp...","seq":"123"}]}</code>,带幂等键。</li>
|
||
<li>返回 <code>created_count,existing_count,failed[]</code>;同一用户对同一 <code>topic+seq</code> 重复收藏不新增记录。</li>
|
||
<li>收藏保存原消息引用、不可变展示快照和附件授权关系;成功 Toast 只在至少一项成功时展示,部分失败需给出汇总。</li>
|
||
</ul>
|
||
<h3>4.17 转发-单选.png</h3>
|
||
<figure><img src="images/转发-单选.png" alt="转发-单选"><figcaption>转发-单选</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>目标列表:<code>GET /v1/im/forward-targets?query=&cursor=</code>,只返回 <code>can_receive_forward=true</code> 的会话和文件助手。</li>
|
||
<li>选中后本地进入确认页,不立即发送;服务端在最终转发接口再次校验目标 Topic、原消息和附件权限。</li>
|
||
</ul>
|
||
<h3>4.18 转发-多选.png</h3>
|
||
<figure><img src="images/转发-多选.png" alt="转发-多选"><figcaption>转发-多选</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>目标查询同 4.17;多选最多取 <code>bootstrap.limits.max_forward_targets</code>,首版建议 9。</li>
|
||
<li>确认前只保存目标 Topic 字符串;不得缓存或提交 <code>tenant_id</code>。跨租户 Topic 在服务端统一拒绝。</li>
|
||
</ul>
|
||
<h3>4.19 转发—逐条转发.png</h3>
|
||
<figure><img src="images/转发—逐条转发.png" alt="转发—逐条转发"><figcaption>转发—逐条转发</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>POST /v1/im/messages/forward</code>,Body <code>{"mode":"separate","sources":[{"topic":"p2p...","seq":"12"}],"target_topics":["grp..."],"comment":"..."}</code>。</li>
|
||
<li>返回每个目标和源消息的 <code>client_mid/seq/status</code>;服务端保持源顺序,重新生成目标消息,不能复用原 <code>seq</code>。</li>
|
||
<li>附件必须建立新的授权引用;原消息已撤回、过期或不可见时该项失败。留言作为每个目标的一条独立文本消息发送。</li>
|
||
</ul>
|
||
<h3>4.20 转发—合并转发—多选状态.png</h3>
|
||
<figure><img src="images/转发—合并转发—多选状态.png" alt="转发—合并转发—多选状态"><figcaption>转发—合并转发—多选状态</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 4.19 接口,<code>mode:"bundle"</code>;服务端生成 <code>forward_bundle</code>,内容含 <code>bundle_id,title,item_count,preview[]</code>,完整内容通过 <code>GET /v1/im/forward-bundles/{bundle_id}?cursor=</code> 获取。</li>
|
||
<li>快照保留显示名、时间和内容摘要,对撤回/敏感字段按策略脱敏;不得允许接收端利用 Bundle 获取原会话额外权限。</li>
|
||
<li>多目标共享不可变 Bundle 内容但分别生成目标消息与授权记录。</li>
|
||
</ul>
|
||
<h2>5. 通讯录、好友关系与黑名单</h2>
|
||
<h3>5.1 通讯录.png</h3>
|
||
<figure><img src="images/通讯录.png" alt="通讯录"><figcaption>通讯录</figcaption></figure>
|
||
<p><strong>状态:新增产品通讯录接口。</strong></p>
|
||
<ul>
|
||
<li>好友列表:<code>GET /v1/im/friends?query=&initial=&cursor=</code>,返回 <code>user_id,display_name,remark,avatar_file_id,initial,online_visibility</code>;显示名优先级为备注名、昵称、账号。</li>
|
||
<li>分页:<code>next_cursor</code> 当前按最后一条好友的 <code>peer_id</code> 生成,客户端必须作为不透明字符串原样回传,不要自行解析、排序或拼接。</li>
|
||
<li>组织成员:<code>GET /v1/im/organization/members?query=&department_id=&cursor=</code>;好友和组织成员是两种关系,前端不得混为一个可发消息权限。</li>
|
||
<li>新朋友未处理数、群列表、文件助手和系统消息入口由 <code>GET /v1/im/bootstrap</code> 返回。列表缓存以租户和用户隔离。</li>
|
||
</ul>
|
||
<h3>5.2 新朋友.png</h3>
|
||
<figure><img src="images/新朋友.png" alt="新朋友"><figcaption>新朋友</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>列表:<code>GET /v1/im/friend-requests?direction=inbound&status=all&cursor=</code>,返回 <code>request_id,applicant,message,source,status,created_at,handled_at</code>。</li>
|
||
<li>同意:<code>POST /v1/im/friend-requests/{request_id}/accept</code>;拒绝:<code>POST /v1/im/friend-requests/{request_id}/reject</code>,均带幂等键和可选 <code>version</code>。</li>
|
||
<li>成功返回最终 <code>status</code> 和 P2P <code>topic</code>。重复审批返回原结果;并发冲突返回 <code>REQUEST_ALREADY_HANDLED</code> 并附当前状态,前端刷新该项。</li>
|
||
</ul>
|
||
<h3>5.3 添加好友.png</h3>
|
||
<figure><img src="images/添加好友.png" alt="添加好友"><figcaption>添加好友</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>账号搜索:<code>GET /v1/im/users/search?q=&type=account&cursor=</code>;扫码后调用 <code>POST /v1/im/qrcodes/resolve</code>,Body <code>{"token":"...","purpose":"add_friend"}</code>。</li>
|
||
<li>结果统一返回 <code>relationship=none|friend|pending_inbound|pending_outbound</code> 和 <code>can_add</code>;隐私不允许或未找到统一为 <code>USER_NOT_DISCOVERABLE</code>。</li>
|
||
<li>分享/二维码见 5.4;发申请见 5.6。服务端按租户配置 <code>allow_user_search</code> 和用户隐私再次拦截。</li>
|
||
</ul>
|
||
<h3>5.4 添加好友(1).png</h3>
|
||
<p>!<a href="images/添加好友(1">添加好友(1)</a>.png)</p>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>获取二维码:<code>POST /v1/im/me/qrcodes</code>,Body <code>{"purpose":"add_friend","expires_in_seconds":86400}</code>,返回 <code>qr_token,qr_payload,expires_at,display_profile</code>。</li>
|
||
<li>二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或 access token。</li>
|
||
<li>分享/保存图片是客户端动作;扫码解析始终调用 5.3,过期返回 <code>QR_CODE_EXPIRED</code>,篡改返回 <code>QR_CODE_INVALID</code>。</li>
|
||
</ul>
|
||
<h3>5.5 添加好友-输入精准用户ID.png</h3>
|
||
<figure><img src="images/添加好友-输入精准用户ID.png" alt="添加好友-输入精准用户ID"><figcaption>添加好友-输入精准用户ID</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>精准查询仍调用 <code>GET /v1/im/users/search?q=&type=exact_account</code>;手机号查询仅当租户和目标隐私同时允许时使用 <code>type=phone</code>。</li>
|
||
<li>每项返回公开资料和 <code>action=add|chat|pending|none</code>。前端不得根据是否返回手机号推断账户存在。</li>
|
||
<li>搜索限流返回 <code>SEARCH_RATE_LIMITED</code>;禁止客户端批量枚举 ID。</li>
|
||
</ul>
|
||
<h3>5.6 添加好友验证.png</h3>
|
||
<figure><img src="images/添加好友验证.png" alt="添加好友验证"><figcaption>添加好友验证</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>POST /v1/im/friend-requests</code>,Body <code>{"target_user_id":"usr...","message":"验证留言","source":"search|qr|group","source_topic":"grp..."}</code>。</li>
|
||
<li>留言最多 50 个 Unicode 字符,服务端做敏感内容审核;<code>source_topic</code> 只能是双方当前可见的同租户群。</li>
|
||
<li>成功返回 <code>request_id,status:"pending"</code>;重复有效申请返回原记录,目标隐私关闭时返回统一 <code>FRIEND_REQUEST_NOT_ALLOWED</code>。</li>
|
||
</ul>
|
||
<h3>5.7 好友个人主页.png</h3>
|
||
<figure><img src="images/好友个人主页.png" alt="好友个人主页"><figcaption>好友个人主页</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/users/{user_id}/profile</code> 返回按当前查看人裁剪后的 <code>display_name,remark,avatar_file_id,gender,region,public_account,signature,relationship,capabilities</code>。</li>
|
||
<li>发消息使用返回的 P2P <code>topic</code>;没有 Topic 时调用 <code>POST /v1/im/conversations/p2p</code>,Body <code>{"peer_user_id":"..."}</code> 并由服务端验证好友关系。</li>
|
||
<li>音视频按钮只按 <code>capabilities.can_call</code> 展示,真正呼叫时再次鉴权。隐藏字段必须从响应中省略,不能只由前端遮挡。</li>
|
||
</ul>
|
||
<h3>5.8 群聊—非好友个人主页.png</h3>
|
||
<figure><img src="images/群聊—非好友个人主页.png" alt="群聊—非好友个人主页"><figcaption>群聊—非好友个人主页</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/members/{user_id}/profile</code> 返回群内昵称、角色和允许公开的账号资料。</li>
|
||
<li><code>can_add_friend</code> 同时受群 <code>allow_member_add_friend</code>、目标隐私和租户策略控制;为 false 时隐藏添加入口。</li>
|
||
<li>从该页申请时 5.6 传 <code>source:"group"</code> 和当前 Topic;服务端校验双方仍在群内。</li>
|
||
</ul>
|
||
<h3>5.9 好友—聊天设置—未拉黑状态.png</h3>
|
||
<figure><img src="images/好友—聊天设置—未拉黑状态.png" alt="好友—聊天设置—未拉黑状态"><figcaption>好友—聊天设置—未拉黑状态</figcaption></figure>
|
||
<p><strong>状态:部分 Topic 设置已实现;产品设置新增。</strong></p>
|
||
<ul>
|
||
<li>查询:<code>GET /v1/im/conversations/{topic}/settings</code> 返回 <code>pinned,mute_notifications,message_retention,blocked,remark,permissions</code>。</li>
|
||
<li>更新置顶/免打扰/保存方式:<code>PATCH /v1/im/conversations/{topic}/settings</code>,Body 只提交变更字段并带 <code>version</code>;返回最新完整设置。</li>
|
||
<li>备注:<code>PATCH /v1/im/friends/{user_id}</code>,Body <code>{"remark":"..."}</code>;查记录见第 7 章,举报见第 11 章,删除见 5.11。</li>
|
||
</ul>
|
||
<h3>5.10 好友—聊天设置—拉黑状态.png</h3>
|
||
<figure><img src="images/好友—聊天设置—拉黑状态.png" alt="好友—聊天设置—拉黑状态"><figcaption>好友—聊天设置—拉黑状态</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>开启拉黑:<code>PUT /v1/im/blocks/{user_id}</code>;解除:<code>DELETE /v1/im/blocks/{user_id}</code>,均带幂等键。</li>
|
||
<li>成功返回 <code>blocked,relationship,updated_at</code> 并向当前用户其他设备推送 <code>privacy.block.updated</code>。拉黑后服务端拦截对方消息和呼叫。</li>
|
||
<li>解除拉黑只恢复黑名单 ACL;若双方已非好友,<code>relationship</code> 仍为 <code>none</code>,不能自动恢复好友。</li>
|
||
</ul>
|
||
<h3>5.11 删除好友提示.png</h3>
|
||
<figure><img src="images/删除好友提示.png" alt="删除好友提示"><figcaption>删除好友提示</figcaption></figure>
|
||
<p><strong>状态:确认框纯客户端;删除接口新增。</strong></p>
|
||
<ul>
|
||
<li>确认调用 <code>DELETE /v1/im/friends/{user_id}</code>,带幂等键。是否清除本地历史由客户端在成功后独立处理,不放入删除好友请求。</li>
|
||
<li>服务端解除双向好友关系但不替对方删除历史;返回 <code>relationship:"none",topic</code>。当前用户是否删除本地/服务端可见记录是独立选择。</li>
|
||
<li>取消不请求;确认后清理页面缓存并等待关系事件校正其他设备。</li>
|
||
</ul>
|
||
<h3>5.12 黑名单.png</h3>
|
||
<figure><img src="images/黑名单.png" alt="黑名单"><figcaption>黑名单</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/blocks?cursor=</code> 返回 <code>user,blocked_at</code>;移出调用 5.10 的 DELETE。</li>
|
||
<li>批量解除:<code>POST /v1/im/blocks/batch-delete</code>,Body <code>{"user_ids":[...]}</code>,返回逐项结果;最多 100 人。</li>
|
||
<li>用户资料不可见时仍用受控占位显示黑名单记录,不能因资料删除而无法解除。</li>
|
||
</ul>
|
||
<h3>5.13 黑名单提示(1).png</h3>
|
||
<p>!<a href="images/黑名单提示(1">黑名单提示(1)</a>.png)</p>
|
||
<p><strong>状态:确认框纯客户端;写接口新增。</strong></p>
|
||
<ul>
|
||
<li>确认后调用 <code>PUT /v1/im/blocks/{user_id}</code>,相同幂等键重复提交返回当前 <code>blocked=true</code>。</li>
|
||
<li>服务端在同一事务内更新黑名单和通信 ACL,并写审计;取消不调用接口。</li>
|
||
<li>若对象跨租户或不存在统一返回 <code>RESOURCE_NOT_FOUND</code>。</li>
|
||
</ul>
|
||
<h3>5.14 黑名单提示.png</h3>
|
||
<figure><img src="images/黑名单提示.png" alt="黑名单提示"><figcaption>黑名单提示</figcaption></figure>
|
||
<p><strong>状态:图片文件名与画面语义不一致;按“清除聊天记录”实施,需改造。</strong></p>
|
||
<ul>
|
||
<li>确认调用 Tinode <code>{del what:"msg",topic,delseq:[{"low":1,"hi":last_seq+1}],hard:false}</code>,仅删除当前用户视图;或封装 <code>DELETE /v1/im/conversations/{topic}/messages</code>。</li>
|
||
<li>不解除好友、不影响对方历史、不删除收藏;附件仅在无任何有效引用且过保留期后回收。</li>
|
||
<li>前端需在确认文案明确“仅清除自己的聊天记录”,成功后清本地 Topic 消息缓存并保留会话关系。</li>
|
||
</ul>
|
||
<h2>6. 群聊创建、资料维护与成员治理</h2>
|
||
<h3>6.1 群聊.png</h3>
|
||
<figure><img src="images/群聊.png" alt="群聊"><figcaption>群聊</figcaption></figure>
|
||
<p><strong>状态:Tinode 群 Topic 已实现基础能力;产品群列表与配额需改造。</strong></p>
|
||
<ul>
|
||
<li>列表:<code>GET /v1/im/groups?category=owned|managed|joined&query=&cursor=</code>,返回 <code>topic,name,avatar_file_id,member_count,my_role,updated_at</code>。</li>
|
||
<li>创建:<code>POST /v1/im/groups</code>,Body <code>{"name":"...","member_user_ids":[],"avatar_file_id":"..."}</code>;原子校验租户群数上限和群成员上限,返回 <code>topic</code>。</li>
|
||
<li>分类由服务端 <code>my_role</code> 决定;创建成功或角色变化后通过系统事件刷新,不由前端自行移动分组。</li>
|
||
</ul>
|
||
<h3>6.2 空白页.png</h3>
|
||
<figure><img src="images/空白页.png" alt="空白页"><figcaption>空白页</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>对 6.1 任一分类返回 <code>items=[]</code> 且 <code>has_more=false</code> 时显示对应空状态,不追加空白占位数据。</li>
|
||
<li>收到 <code>group.membership.updated</code> 或创建成功后重新拉取当前分类。</li>
|
||
</ul>
|
||
<h3>6.3 加入群聊.png</h3>
|
||
<figure><img src="images/加入群聊.png" alt="加入群聊"><figcaption>加入群聊</figcaption></figure>
|
||
<p><strong>状态:新增邀请卡片和加入接口。</strong></p>
|
||
<ul>
|
||
<li>邀请卡下行 <code>system_event{event_type:"group_invitation",invitation_id,group,inviter,status,expires_at}</code>。</li>
|
||
<li>点击加入:<code>POST /v1/im/group-invitations/{invitation_id}/accept</code>,带幂等键;返回 <code>status=joined|pending_approval</code> 和 <code>topic</code>。</li>
|
||
<li>服务端校验邀请租户、有效期、群状态、成员资格和成员数上限;重复加入返回当前状态,过期返回 <code>INVITATION_EXPIRED</code>。</li>
|
||
</ul>
|
||
<h3>6.4 内容沟通对话3-开启进群验证与拉人状态.png</h3>
|
||
<figure><img src="images/内容沟通对话3-开启进群验证与拉人状态.png" alt="内容沟通对话3-开启进群验证与拉人状态"><figcaption>内容沟通对话3-开启进群验证与拉人状态</figcaption></figure>
|
||
<p><strong>状态:新增群成员事件。</strong></p>
|
||
<ul>
|
||
<li>邀请:<code>POST /v1/im/groups/{topic}/invitations</code>,Body <code>{"user_ids":[],"message":""}</code>;按群策略直接加入或创建待审批申请。</li>
|
||
<li>加入、审批、拒绝等结果由服务端生成 <code>system_event{event_type:"group_membership_changed",operator,target_users,action,result}</code> 并写入群时间线。</li>
|
||
<li>客户端只渲染可信系统事件;服务端同时更新成员表、Topic 订阅和成员数,不允许三者不一致。</li>
|
||
</ul>
|
||
<h3>6.5 邀请进群申请.png</h3>
|
||
<figure><img src="images/邀请进群申请.png" alt="邀请进群申请"><figcaption>邀请进群申请</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/join-requests?status=pending&cursor=</code> 返回申请人、邀请人、来源、时间、状态和 <code>version</code>。</li>
|
||
<li>同意:<code>POST /v1/im/groups/{topic}/join-requests/{id}/accept</code>;拒绝:<code>POST /v1/im/groups/{topic}/join-requests/{id}/reject</code>,带幂等键与 <code>version</code>。</li>
|
||
<li>仅群主/管理员可处理;同意时原子校验群成员上限。并发处理返回 <code>REQUEST_ALREADY_HANDLED</code> 和最终状态。</li>
|
||
</ul>
|
||
<h3>6.6 新邀请进群申请.png</h3>
|
||
<figure><img src="images/新邀请进群申请.png" alt="新邀请进群申请"><figcaption>新邀请进群申请</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/summary</code> 返回 <code>pending_join_request_count</code>、最新公告和权限;新申请通过 <code>group.join_request.created</code> 推送给有审批权限的成员。</li>
|
||
<li>审批完成后服务端广播新数量;前端归零时移除入口。普通成员响应中不得得到申请人敏感信息或数量。</li>
|
||
</ul>
|
||
<h3>6.7 群公告状态.png</h3>
|
||
<figure><img src="images/群公告状态.png" alt="群公告状态"><figcaption>群公告状态</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>查询最新公告:<code>GET /v1/im/groups/{topic}/announcements/latest</code>;发布/更新:<code>PUT /v1/im/groups/{topic}/announcements/current</code>,Body <code>{"content":"...","version":1}</code>。</li>
|
||
<li>返回 <code>announcement_id,content,author,version,published_at</code>。仅有权限角色可写,使用乐观锁防覆盖。</li>
|
||
<li>发布成功后服务端生成群系统事件;顶部摘要使用接口返回数据,不解析聊天文本猜测。</li>
|
||
</ul>
|
||
<h3>6.8 群公告状态/邀请进群申请同时存在.png</h3>
|
||
<figure><img src="images/群公告状态/邀请进群申请同时存在.png" alt="群公告状态/邀请进群申请同时存在"><figcaption>群公告状态/邀请进群申请同时存在</figcaption></figure>
|
||
<p><strong>状态:纯客户端组合;数据接口新增。</strong></p>
|
||
<ul>
|
||
<li>页面一次调用 <code>GET /v1/im/groups/{topic}/summary</code>,分别读取 <code>announcement</code> 和 <code>pending_join_request_count</code>,按产品固定顺序渲染两个入口。</li>
|
||
<li>处理申请只刷新计数,不清公告;更新公告只替换公告对象,不影响申请入口。</li>
|
||
</ul>
|
||
<h3>6.9 群公告(对方查看状态).png</h3>
|
||
<figure><img src="images/群公告(对方查看状态).png" alt="群公告(对方查看状态)"><figcaption>群公告(对方查看状态)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>详情:<code>GET /v1/im/groups/{topic}/announcements/{announcement_id}</code>;普通成员响应不含编辑权限。</li>
|
||
<li>已读:<code>POST /v1/im/groups/{topic}/announcements/{id}/read</code>,带幂等键,返回 <code>read_at</code>;用于清除新公告标记。</li>
|
||
<li>服务端校验当前群成员资格,退群后不可借历史 ID 读取新公告。</li>
|
||
</ul>
|
||
<h3>6.10 群聊设置.png</h3>
|
||
<figure><img src="images/群聊设置.png" alt="群聊设置"><figcaption>群聊设置</figcaption></figure>
|
||
<p><strong>状态:部分订阅设置已实现;产品菜单新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/settings</code> 返回 <code>my_role,permissions,profile,announcement,member_count,pinned,mute_notifications</code> 和群治理开关。</li>
|
||
<li>个人置顶/免打扰通过 <code>PATCH /v1/im/groups/{topic}/member-settings</code>;群级设置必须使用 6.17,不混在个人设置里。</li>
|
||
<li>退出:<code>POST /v1/im/groups/{topic}/leave</code>;群主必须先转让或解散。清记录、举报分别见 5.14、11.3。</li>
|
||
</ul>
|
||
<h3>6.11 群资料.png</h3>
|
||
<figure><img src="images/群资料.png" alt="群资料"><figcaption>群资料</figcaption></figure>
|
||
<p><strong>状态:需改造。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}</code> 返回 <code>name,avatar_file_id,description,owner,member_count,version,permissions</code>。</li>
|
||
<li>更新:<code>PATCH /v1/im/groups/{topic}</code>,Body 可含 <code>name,avatar_file_id,description,version</code>;成功返回最新对象并推送 <code>group.profile.updated</code>。</li>
|
||
<li>服务端做角色、长度、敏感内容、文件归属校验;前端只展示 <code>permissions.can_edit_profile</code> 允许的入口。</li>
|
||
</ul>
|
||
<h3>6.12 设置群头像.png</h3>
|
||
<figure><img src="images/设置群头像.png" alt="设置群头像"><figcaption>设置群头像</figcaption></figure>
|
||
<p><strong>状态:上传基础已实现;用途绑定需改造。</strong></p>
|
||
<ul>
|
||
<li>来源选择、裁剪是客户端动作。上传使用 <code>purpose=group_avatar</code>,服务端校验图片 MIME、尺寸、大小并返回 <code>file_id</code>。</li>
|
||
<li>上传后调用 6.11 PATCH <code>{"avatar_file_id":"...","version":n}</code>;只有 PATCH 成功才替换正式头像。</li>
|
||
<li>取消或 PATCH 失败的临时文件按 TTL 回收,不能污染群文件列表。</li>
|
||
</ul>
|
||
<h3>6.13 设置昵称(1).png</h3>
|
||
<p>!<a href="images/设置昵称(1">设置昵称(1)</a>.png)</p>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>PUT /v1/im/groups/{topic}/members/me/profile</code>,Body <code>{"nickname":"...","version":n}</code>。</li>
|
||
<li>服务端校验成员资格、长度和敏感词,返回 <code>nickname,version,updated_at</code> 并推送成员资料变化。</li>
|
||
<li>该昵称仅作用当前群,不能修改全局用户昵称或其他群资料。</li>
|
||
</ul>
|
||
<h3>6.14 群简介.png</h3>
|
||
<figure><img src="images/群简介.png" alt="群简介"><figcaption>群简介</figcaption></figure>
|
||
<p><strong>状态:同 6.11,需改造。</strong></p>
|
||
<ul>
|
||
<li>保存调用 <code>PATCH /v1/im/groups/{topic}</code>,Body <code>{"description":"...","version":n}</code>;空字符串表示清空,省略表示不修改。</li>
|
||
<li>服务端返回 <code>VALIDATION_FAILED</code> 的 <code>details.field/max_length</code> 或 <code>CONTENT_REJECTED</code>;前端保留草稿供修正。</li>
|
||
</ul>
|
||
<h3>6.15 群标识.png</h3>
|
||
<figure><img src="images/群标识.png" alt="群标识"><figcaption>群标识</figcaption></figure>
|
||
<p><strong>状态:角色已有基础数据;产品渲染需统一。</strong></p>
|
||
<ul>
|
||
<li>群消息下行增加服务端确认的 <code>sender_snapshot{display_name,role}</code>,角色枚举固定 <code>owner|admin|member</code>。</li>
|
||
<li>成员角色变化事件到达后刷新成员缓存;历史消息可以保留发送时角色快照,详情以当前角色接口为准。</li>
|
||
<li>客户端不得根据本地管理员列表给消息自行加可信标识。</li>
|
||
</ul>
|
||
<h3>6.16 群二维码页面.png</h3>
|
||
<figure><img src="images/群二维码页面.png" alt="群二维码页面"><figcaption>群二维码页面</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>POST /v1/im/groups/{topic}/qrcodes</code>,Body <code>{"expires_in_seconds":86400}</code>,返回签名 <code>qr_token,qr_payload,expires_at,group_snapshot</code>。</li>
|
||
<li>扫码调用 <code>POST /v1/im/qrcodes/resolve</code>,<code>purpose:"join_group"</code>;随后 <code>POST /v1/im/groups/{topic}/join-requests</code> 并携带 <code>qr_token</code>。</li>
|
||
<li>群解散、二维码过期或管理员撤销后必须失效;二维码不得直接承载内部群权限。</li>
|
||
</ul>
|
||
<h3>6.17 群组管理.png</h3>
|
||
<figure><img src="images/群组管理.png" alt="群组管理"><figcaption>群组管理</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>查询/更新:<code>GET/PATCH /v1/im/groups/{topic}/governance</code>,字段为 <code>join_approval,mute_all,member_upload_files,member_private_chat,member_invite</code> 和 <code>version</code>。</li>
|
||
<li>每个字段返回 <code>editable</code>;PATCH 仅群主或被授权管理员可用,记录操作者、旧值、新值和 Request ID。</li>
|
||
<li>解散:<code>DELETE /v1/im/groups/{topic}</code>,需二次 <code>X-Action-Token</code> 和幂等键;成功后撤销 Topic、二维码、邀请和文件新增权限。</li>
|
||
</ul>
|
||
<h3>6.18 设置群管理员.png</h3>
|
||
<figure><img src="images/设置群管理员.png" alt="设置群管理员"><figcaption>设置群管理员</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/members?role=admin&cursor=</code> 返回管理员列表;<code>GET /v1/im/groups/{topic}/role-limits</code> 返回 <code>max_admin_count,current_admin_count</code>。</li>
|
||
<li>只有群主可进入。群主本人以 <code>owner</code> 单独展示,不计入普通管理员列表和名额。</li>
|
||
</ul>
|
||
<h3>6.19 添加管理员.png</h3>
|
||
<figure><img src="images/添加管理员.png" alt="添加管理员"><figcaption>添加管理员</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>候选:<code>GET /v1/im/groups/{topic}/members?eligible_role=admin&query=&cursor=</code>,返回 <code>eligible</code> 和不可选原因。</li>
|
||
<li>提交:<code>POST /v1/im/groups/{topic}/admins/batch</code>,Body <code>{"user_ids":[],"version":n}</code>;服务端原子校验群主身份、成员资格和管理员上限。</li>
|
||
<li>超限返回 <code>GROUP_ADMIN_LIMIT_EXCEEDED</code>,并附 <code>max/current/requested</code>。</li>
|
||
</ul>
|
||
<h3>6.20 添加管理员-设置成功.png</h3>
|
||
<figure><img src="images/添加管理员-设置成功.png" alt="添加管理员-设置成功"><figcaption>添加管理员-设置成功</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>6.19 成功返回 <code>admins,version,updated_at</code>;前端用响应替换列表并显示成功,不在请求完成前自行修改角色。</li>
|
||
<li>服务端推送 <code>group.role.updated</code>,新管理员收到后刷新菜单权限和群标识。</li>
|
||
</ul>
|
||
<h3>6.21 添加管理员-设置成功(1).png</h3>
|
||
<p>!<a href="images/添加管理员-设置成功(1">添加管理员-设置成功(1)</a>.png)</p>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>提交期间按幂等键禁重复点击。批量接口返回 <code>succeeded[]</code>、<code>failed[{user_id,reason}]</code> 和最新 <code>admins</code>。</li>
|
||
<li>部分失败时保留成功角色,不做整批客户端回滚;前端展示失败原因并以服务端列表为准。</li>
|
||
</ul>
|
||
<h3>6.22 删除管理员-设置成功.png</h3>
|
||
<figure><img src="images/删除管理员-设置成功.png" alt="删除管理员-设置成功"><figcaption>删除管理员-设置成功</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>DELETE /v1/im/groups/{topic}/admins/{user_id}</code>,带幂等键和 <code>If-Match</code>/<code>version</code>。</li>
|
||
<li>成功后目标角色变为 <code>member</code>,保留群成员关系;服务端立即撤销管理权限并广播角色事件。</li>
|
||
<li>删除群主、非成员或最后状态冲突分别返回稳定错误,不允许前端拼接通用成功。</li>
|
||
</ul>
|
||
<h3>6.23 添加禁言成员.png</h3>
|
||
<figure><img src="images/添加禁言成员.png" alt="添加禁言成员"><figcaption>添加禁言成员</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>候选:<code>GET /v1/im/groups/{topic}/members?mute_eligible=true&query=&cursor=</code>。</li>
|
||
<li>提交:<code>POST /v1/im/groups/{topic}/mutes/batch</code>,Body <code>{"members":[{"user_id":"usr...","expires_at":null}],"reason":""}</code>。</li>
|
||
<li>服务端校验操作者角色层级;管理员不能禁言群主,普通管理员互相操作规则由治理策略确定。</li>
|
||
</ul>
|
||
<h3>6.24 禁言的群成员.png</h3>
|
||
<figure><img src="images/禁言的群成员.png" alt="禁言的群成员"><figcaption>禁言的群成员</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/mutes?cursor=</code> 返回 <code>user,muted_by,muted_at,expires_at</code>;解除 <code>DELETE /v1/im/groups/{topic}/mutes/{user_id}</code>。</li>
|
||
<li>禁言成员发送消息时服务端返回 <code>GROUP_MUTED</code> 和可选 <code>expires_at</code>;不能只禁用前端输入框。</li>
|
||
<li>退群时清理禁言记录;过期由服务端自动失效并推送更新。</li>
|
||
</ul>
|
||
<h3>6.25 选择新群主.png</h3>
|
||
<figure><img src="images/选择新群主.png" alt="选择新群主"><figcaption>选择新群主</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/members?eligible_role=owner&query=&cursor=</code>,只返回状态正常的当前成员。</li>
|
||
<li>前端单选并保存 <code>target_user_id</code>;当前群主、已退出、停用账号返回 <code>eligible=false</code>。</li>
|
||
<li>此步骤不修改权限,真正转让仅在 6.26 确认后执行。</li>
|
||
</ul>
|
||
<h3>6.26 选择新群主提示.png</h3>
|
||
<figure><img src="images/选择新群主提示.png" alt="选择新群主提示"><figcaption>选择新群主提示</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>POST /v1/im/groups/{topic}/owner-transfer</code>,Body <code>{"target_user_id":"usr...","version":n}</code>,必须带幂等键和二次认证 <code>X-Action-Token</code>。</li>
|
||
<li>服务端事务内把旧群主降为管理员或成员、目标升为群主、更新权限和审计,再生成系统事件。</li>
|
||
<li>成功返回 <code>owner,previous_owner,roles,version</code>;并发成员变化返回 <code>GROUP_VERSION_CONFLICT</code>,前端重新确认,不自动重试。</li>
|
||
</ul>
|
||
<h3>6.27 群文件.png</h3>
|
||
<figure><img src="images/群文件.png" alt="群文件"><figcaption>群文件</figcaption></figure>
|
||
<p><strong>状态:新增群文件索引;底层文件存储已实现。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/groups/{topic}/files?query=&type=&cursor=</code> 返回 <code>group_file_id,file_id,name,size,mime,uploader,created_at,download_state,permissions</code>。</li>
|
||
<li>下载使用认证文件接口,服务端校验当前群成员和文件状态;删除 <code>DELETE /v1/im/groups/{topic}/files/{group_file_id}</code> 需上传者或管理员权限。</li>
|
||
<li>退群后不可继续使用旧 URL 下载;缓存文件是否保留由客户端隐私策略处理。</li>
|
||
</ul>
|
||
<h3>6.28 群文件-上传状态.png</h3>
|
||
<figure><img src="images/群文件-上传状态.png" alt="群文件-上传状态"><figcaption>群文件-上传状态</figcaption></figure>
|
||
<p><strong>状态:需改造。</strong></p>
|
||
<ul>
|
||
<li>先用 <code>purpose=group_file,topic=...</code> 上传,再 <code>POST /v1/im/groups/{topic}/files</code>,Body <code>{"file_id":"..."}</code> 建立群文件记录。</li>
|
||
<li>服务端校验 <code>member_upload_files</code>、单文件上限、租户总存储和群成员身份;成功返回完整群文件项。</li>
|
||
<li>上传失败可复用 <code>upload_id</code>;登记失败不得在列表显示,孤立文件按 TTL 回收。</li>
|
||
</ul>
|
||
<h2>7. 文件下载、收藏管理与聊天记录检索</h2>
|
||
<h3>7.1 我的收藏.png</h3>
|
||
<figure><img src="images/我的收藏.png" alt="我的收藏"><figcaption>我的收藏</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/favorites?type=all|text|image|video|voice|file|link&q=&cursor=</code>,返回 <code>favorite_id,type,snapshot,source,collected_at,availability</code>。</li>
|
||
<li>类型与关键词由服务端过滤,客户端不下载全部收藏再筛选。<code>source</code> 仅下发用户仍可知晓的会话摘要。</li>
|
||
<li>收藏数据属于当前租户当前用户;跨租户登录不得合并列表。</li>
|
||
</ul>
|
||
<h3>7.2 我的收藏(1).png</h3>
|
||
<p>!<a href="images/我的收藏(1">我的收藏(1)</a>.png)</p>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>详情:<code>GET /v1/im/favorites/{favorite_id}</code>;删除:<code>DELETE /v1/im/favorites/{favorite_id}</code>;转发使用 7.8。</li>
|
||
<li>收藏创建时保存受控快照和附件引用,因此原消息对当前用户删除后仍可按保留策略展示;原资源因合规删除时返回 <code>availability:"removed"</code>。</li>
|
||
<li>删除收藏不删除原消息或已转发消息。</li>
|
||
</ul>
|
||
<h3>7.3 我的收藏-文件.png</h3>
|
||
<figure><img src="images/我的收藏-文件.png" alt="我的收藏-文件"><figcaption>我的收藏-文件</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 7.1,固定 <code>type=file</code>;项中使用 <code>snapshot.file{name,size,mime,file_id}</code> 和 <code>availability</code>。</li>
|
||
<li>点击前调用收藏详情获得当前授权,下载仍走 7.9 文件协议;失效显示原因,不反复请求公共 URL。</li>
|
||
</ul>
|
||
<h3>7.4 我的收藏-文本.png</h3>
|
||
<figure><img src="images/我的收藏-文本.png" alt="我的收藏-文本"><figcaption>我的收藏-文本</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 7.1,固定 <code>type=text</code>;服务端返回纯文本/结构化 Drafty 快照和安全高亮区间 <code>matches[]</code>。</li>
|
||
<li>展开、折叠、复制为本地动作;复制前遵守企业 DLP 策略 <code>permissions.can_copy</code>。</li>
|
||
</ul>
|
||
<h3>7.5 我的收藏-语音.png</h3>
|
||
<figure><img src="images/我的收藏-语音.png" alt="我的收藏-语音"><figcaption>我的收藏-语音</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 7.1,固定 <code>type=voice</code>;播放前获取认证文件,返回 <code>duration_ms,waveform,availability</code>。</li>
|
||
<li>播放/暂停和单实例播放器是客户端状态;服务端不接收逐秒播放进度,除非后续定义跨端收听状态。</li>
|
||
</ul>
|
||
<h3>7.6 我的收藏—链接.png</h3>
|
||
<figure><img src="images/我的收藏—链接.png" alt="我的收藏—链接"><figcaption>我的收藏—链接</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 7.1,固定 <code>type=link</code>;服务端快照返回 <code>url,title,summary,thumbnail_file_id,risk_level</code>。</li>
|
||
<li>点击前 <code>POST /v1/im/security/urls/check</code>,Body <code>{"url":"...","source":"favorite"}</code>;返回 <code>allow|warn|block</code> 和 <code>risk_challenge_id</code>。</li>
|
||
<li>客户端不得在未确认时自动打开或预加载高风险外链。</li>
|
||
</ul>
|
||
<h3>7.7 收藏—文件(多选).png</h3>
|
||
<figure><img src="images/收藏—文件(多选).png" alt="收藏—文件(多选)"><figcaption>收藏—文件(多选)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>选择状态本地维护;批量删除 <code>POST /v1/im/favorites/batch-delete</code>,Body <code>{"favorite_ids":[]}</code>;批量转发见 7.8。</li>
|
||
<li>返回逐项成功/失败;删除必须二次确认并带幂等键。跨页选择只保存 ID,不保留失效的临时下载地址。</li>
|
||
</ul>
|
||
<h3>7.8 收藏文件—分享给好友页面.png</h3>
|
||
<figure><img src="images/收藏文件—分享给好友页面.png" alt="收藏文件—分享给好友页面"><figcaption>收藏文件—分享给好友页面</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>目标列表使用 4.17;提交 <code>POST /v1/im/favorites/forward</code>,Body <code>{"favorite_ids":[],"target_topics":[],"comment":""}</code>。</li>
|
||
<li>服务端从收藏的受控附件引用生成新消息和新 ACL,返回每个目标的消息结果;收藏原件不变。</li>
|
||
<li>文件失效、目标禁言或无发送权限按目标逐项失败,不允许客户端直接复用旧下载 URL 发送。</li>
|
||
</ul>
|
||
<h3>7.9 文件-下载.png</h3>
|
||
<figure><img src="images/文件-下载.png" alt="文件-下载"><figcaption>文件-下载</figcaption></figure>
|
||
<p><strong>状态:底层下载已实现租户校验;资源 ACL 需改造。</strong></p>
|
||
<ul>
|
||
<li>客户端使用 <code>GET /v0/file/s/{file_id}</code>,Header 传 API Key 与 <code>X-Tinode-Auth: token <base64_token></code>;禁止 <code>?auth=&secret=</code>。</li>
|
||
<li>服务端除 Token 租户外,还必须校验该用户通过消息、群文件或收藏拥有有效引用,并支持 <code>Range</code>、<code>ETag</code>、内容长度和安全文件名。</li>
|
||
<li>下载前客户端校验本地空间和网络策略;<code>FILE_FORBIDDEN</code> 不自动重试,<code>FILE_UNAVAILABLE</code> 显示失效。</li>
|
||
</ul>
|
||
<h3>7.10 文件-下载中.png</h3>
|
||
<figure><img src="images/文件-下载中.png" alt="文件-下载中"><figcaption>文件-下载中</figcaption></figure>
|
||
<p><strong>状态:客户端任务管理;服务端需支持 Range。</strong></p>
|
||
<ul>
|
||
<li>下载任务保存 <code>file_id,etag,total_bytes,downloaded_bytes,temp_path,state</code>;恢复时携带 <code>Range</code> 与 <code>If-Range: <etag></code>。</li>
|
||
<li>ETag 改变或服务端返回完整 200 时丢弃旧分片重新下载;取消时关闭连接并清理临时文件。</li>
|
||
<li>Token 过期先完成一次刷新后续传,不能把 Token 写入任务文件或日志。</li>
|
||
</ul>
|
||
<h3>7.11 文件-下载完成.png</h3>
|
||
<figure><img src="images/文件-下载完成.png" alt="文件-下载完成"><figcaption>文件-下载完成</figcaption></figure>
|
||
<p><strong>状态:纯客户端完成态。</strong></p>
|
||
<ul>
|
||
<li>完成后校验长度和服务端提供的 checksum,再原子移动到受控目录;校验失败删除并标记重试。</li>
|
||
<li>打开/分享遵守系统沙箱和企业 DLP。每次展示页面检查本地文件存在性,不存在则恢复“可下载”。</li>
|
||
</ul>
|
||
<h3>7.12 查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容).png</h3>
|
||
<figure><img src="images/查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容).png" alt="查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容)"><figcaption>查找聊天记录-搜索关键词选中状态(点击后跳出对应聊天内容)</figcaption></figure>
|
||
<p><strong>状态:新增服务端搜索。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor=</code> 返回 <code>seq,from,ts,snippet,matches[]</code>。</li>
|
||
<li>点击结果后 <code>GET /v1/im/topics/{topic}/messages/{seq}/context?before=20&after=20</code>,返回锚点及上下文;客户端定位 <code>seq</code> 并高亮。</li>
|
||
<li>服务端只搜索当前用户可见且未硬删除的消息,关键词做长度/频率限制,搜索日志不得记录原文。</li>
|
||
</ul>
|
||
<h3>7.13 查找聊天记录-文件.png</h3>
|
||
<figure><img src="images/查找聊天记录-文件.png" alt="查找聊天记录-文件"><figcaption>查找聊天记录-文件</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 <code>GET /v1/im/topics/{topic}/messages/search?type=file&q=&cursor=</code>,返回消息 <code>seq</code> 与文件快照、发送者、时间、<code>availability</code>。</li>
|
||
<li>预览/下载使用 7.9;撤回或无授权时显示失效。点击来源用 7.12 上下文接口定位。</li>
|
||
</ul>
|
||
<h3>7.14 查找聊天记录-链接.png</h3>
|
||
<figure><img src="images/查找聊天记录-链接.png" alt="查找聊天记录-链接"><figcaption>查找聊天记录-链接</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用搜索接口 <code>type=link</code>,返回 <code>url,title,summary,seq,ts,risk_level</code>;打开前执行 7.6 URL 安全检查。</li>
|
||
<li>链接预览由服务端受控抓取和缓存,客户端不直接抓取内网地址,防止 SSRF 和隐私泄露。</li>
|
||
</ul>
|
||
<h3>7.15 查看聊天记录-图片.png</h3>
|
||
<figure><img src="images/查看聊天记录-图片.png" alt="查看聊天记录-图片"><figcaption>查看聊天记录-图片</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/topics/{topic}/media?type=image&cursor=</code> 返回 <code>seq,thumbnail_file_id,file_id,width,height,ts,availability</code>。</li>
|
||
<li>缩略图懒加载使用认证文件请求;大图前再次授权。多选仅保存 <code>(topic,seq)</code>,操作使用第 4 章批量接口。</li>
|
||
</ul>
|
||
<h3>7.16 查看聊天记录-视频.png</h3>
|
||
<figure><img src="images/查看聊天记录-视频.png" alt="查看聊天记录-视频"><figcaption>查看聊天记录-视频</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>调用 7.15 接口,<code>type=video</code>,额外返回 <code>duration_ms,mime,size</code>。</li>
|
||
<li>播放器使用支持 Range 的认证下载/流式读取;文件失效、Token 过期和网络中断分别映射可理解状态,返回列表保持游标与滚动位置。</li>
|
||
</ul>
|
||
<h2>8. 实时语音与视频通话</h2>
|
||
<h3>8.1 各种通话提示状态.png</h3>
|
||
<figure><img src="images/各种通话提示状态.png" alt="各种通话提示状态"><figcaption>各种通话提示状态</figcaption></figure>
|
||
<p><strong>状态:已实现基础通话记录替换;产品文案映射需统一。</strong></p>
|
||
<ul>
|
||
<li>发起呼叫时的持久消息就是通话记录。服务端以相同 <code>seq</code> 写 replacement <code>{data}</code>,<code>head.replace=":{seq}"</code>,并把 <code>head.webrtc</code> 更新为 <code>accepted|finished|disconnected|missed|declined</code>。</li>
|
||
<li>已接通结束时 <code>head.webrtc-duration</code> 为服务端计算的毫秒数。客户端映射:<code>finished=已结束</code>、<code>missed=未接听/已取消</code>、<code>declined=已拒绝</code>、<code>disconnected=异常结束</code>。</li>
|
||
<li>双方使用原消息 <code>content.callId/callType</code> 和同一 <code>seq</code>;客户端不得另发一条“通话结果”消息或相信本地传入的 duration。</li>
|
||
</ul>
|
||
<h3>8.2 对方状态.png</h3>
|
||
<figure><img src="images/对方状态.png" alt="对方状态"><figcaption>对方状态</figcaption></figure>
|
||
<p><strong>状态:在线邀请已实现;Push 与产品鉴权需改造。</strong></p>
|
||
<ul>
|
||
<li>来电是 Topic 下行的初始 <code>{data}</code>:<code>head.webrtc="started"</code>、<code>head.x-im-type="call"</code>,<code>content</code> 至少含 <code>callId,callType,audio,video</code>。客户端保存该消息 <code>seq</code> 作为后续所有 call note 的关联 ID。</li>
|
||
<li>来电端开始响铃后发送 <code>{note:{topic,what:"call",event:"ringing",seq,payload:{"callId":"..."}}}</code>;接听发送 <code>event:"accept"</code>;拒绝发送 <code>event:"hang-up"</code>。</li>
|
||
<li>后台 Push 只携带 <code>callId,topic,seq,caller_display,callType,expires_at</code>,不含 SDP、ICE 或 Token;用户操作后必须回 WebSocket 鉴权。</li>
|
||
</ul>
|
||
<h3>8.3 语音通话-对方待接受状态.png</h3>
|
||
<figure><img src="images/语音通话-对方待接受状态.png" alt="语音通话-对方待接受状态"><figcaption>语音通话-对方待接受状态</figcaption></figure>
|
||
<p><strong>状态:发起与超时已实现基础;好友/拉黑/租户开关校验需改造。</strong></p>
|
||
<ul>
|
||
<li>发起前查询 <code>capabilities.can_call</code>,再发布持久邀请消息:</li>
|
||
</ul>
|
||
<pre><code class="language-json">
|
||
{"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}}}
|
||
</code></pre>
|
||
<ul>
|
||
<li><code>{ctrl code:202,params.seq}</code> 的 <code>seq</code> 是后续 <code>ringing/accept/offer/answer/ice-candidate/hang-up</code> 必填关联值。服务端只允许 P2P 双方参与,并校验好友、拉黑、租户通话开关和当前 Topic 无并发呼叫。</li>
|
||
<li>发起方取消发送 <code>{note event:"hang-up",seq,payload:{"callId":"...","reason":"cancelled"}}</code>;等待超时由服务端结束并把原消息替换为 <code>missed</code>。</li>
|
||
</ul>
|
||
<h3>8.4 语音通话-双方语音沟通中.png</h3>
|
||
<figure><img src="images/语音通话-双方语音沟通中.png" alt="语音通话-双方语音沟通中"><figcaption>语音通话-双方语音沟通中</figcaption></figure>
|
||
<p><strong>状态:WebRTC 客户端能力;信令和服务端计时已实现。</strong></p>
|
||
<ul>
|
||
<li>被叫发送 <code>accept</code> 后,服务端把原消息替换为 <code>webrtc:"accepted"</code> 并向主叫发送 <code>{info what:"call",event:"accept",seq}</code>。</li>
|
||
<li>双方随后用 <code>{note what:"call",event:"offer|answer|ice-candidate",seq,payload:{"callId":"...",...}}</code> 交换 SDP/ICE。ICE 配置使用 <code>hi.ctrl.params.iceServers</code>,不得在 App 中硬编码 TURN 长期凭证。</li>
|
||
<li>任一端发送 <code>hang-up</code>,服务端按接受时间计算 duration 并替换原通话消息。异常断线后按原 <code>topic+seq+callId</code> 恢复或结束,不新建第二通呼叫。</li>
|
||
</ul>
|
||
<h3>8.5 语音通话-麦克风与扬声器已关.png</h3>
|
||
<figure><img src="images/语音通话-麦克风与扬声器已关.png" alt="语音通话-麦克风与扬声器已关"><figcaption>语音通话-麦克风与扬声器已关</figcaption></figure>
|
||
<p><strong>状态:纯客户端设备控制。</strong></p>
|
||
<ul>
|
||
<li>麦克风关闭通过禁用本地音轨,扬声器关闭/听筒切换通过系统音频路由完成,不调用业务 HTTP。</li>
|
||
<li>UI 必须读取实际音轨和路由状态;权限撤销时强制更新。是否告知对方静音可发送临时 <code>media-state</code>,不落聊天记录。</li>
|
||
</ul>
|
||
<h3>8.6 语音通话-麦克风与扬声器已开.png</h3>
|
||
<figure><img src="images/语音通话-麦克风与扬声器已开.png" alt="语音通话-麦克风与扬声器已开"><figcaption>语音通话-麦克风与扬声器已开</figcaption></figure>
|
||
<p><strong>状态:纯客户端设备控制。</strong></p>
|
||
<ul>
|
||
<li>开启麦克风恢复本地音轨,开启扬声器选择实际输出设备;蓝牙/耳机变化以操作系统回调为准。</li>
|
||
<li>不把设备名称、系统音量上传服务端。切换失败恢复真实按钮状态并给本地提示。</li>
|
||
</ul>
|
||
<h3>8.7 视频通话—等待对方接受接受邀请中.png</h3>
|
||
<figure><img src="images/视频通话—等待对方接受接受邀请中.png" alt="视频通话—等待对方接受接受邀请中"><figcaption>视频通话—等待对方接受接受邀请中</figcaption></figure>
|
||
<p><strong>状态:基础通话信令已实现;隐私约束需落实。</strong></p>
|
||
<ul>
|
||
<li>发起协议同 8.3,<code>media_type:"video"</code>。本地预览只在设备显示,接听前媒体轨默认不发给对方,SDP 只声明能力。</li>
|
||
<li>切换摄像头纯客户端;取消发送 <code>hang-up:cancelled</code>。相机权限不足时改为音频呼叫或取消,不伪装视频已开启。</li>
|
||
</ul>
|
||
<h3>8.8 视频通话—等待对方接受接受邀请-摄像头关闭.png</h3>
|
||
<figure><img src="images/视频通话—等待对方接受接受邀请-摄像头关闭.png" alt="视频通话—等待对方接受接受邀请-摄像头关闭"><figcaption>视频通话—等待对方接受接受邀请-摄像头关闭</figcaption></figure>
|
||
<p><strong>状态:纯客户端状态;媒体状态事件需改造。</strong></p>
|
||
<ul>
|
||
<li>关闭本地视频轨并显示头像;接听后沿用 <code>video_enabled=false</code>。</li>
|
||
<li>当前服务端只允许 <code>ringing/accept/offer/answer/ice-candidate/hang-up</code>;首版应直接通过 WebRTC 轨道 mute 状态呈现。若后续新增 <code>media-state</code>,必须先修改 <code>server/calls.go</code> 事件白名单、参与者校验和限频,客户端不能提前发送未知事件。</li>
|
||
</ul>
|
||
<h3>8.9 双方视频通话中.png</h3>
|
||
<figure><img src="images/双方视频通话中.png" alt="双方视频通话中"><figcaption>双方视频通话中</figcaption></figure>
|
||
<p><strong>状态:WebRTC 客户端能力;服务端信令已具备基础。</strong></p>
|
||
<ul>
|
||
<li>主/小窗布局、拖动和横竖屏纯客户端;WebRTC offer/answer/ICE 与 8.2~8.4 相同。</li>
|
||
<li>服务端只做身份鉴权、信令路由、TURN 配置和通话审计,不接收前端伪造的通话时长。</li>
|
||
<li>网络恶化可本地降码率或关闭视频轨;最终结束必须发 <code>hang-up</code>。服务端未支持 <code>media-state</code> 前不得发送该事件。</li>
|
||
</ul>
|
||
<h3>8.10 双方视频通话中-对方关闭摄像头状态.png</h3>
|
||
<figure><img src="images/双方视频通话中-对方关闭摄像头状态.png" alt="双方视频通话中-对方关闭摄像头状态"><figcaption>双方视频通话中-对方关闭摄像头状态</figcaption></figure>
|
||
<p><strong>状态:现阶段纯客户端;可选媒体状态事件需改造。</strong></p>
|
||
<ul>
|
||
<li>以对端 WebRTC 视频轨的 mute/unmute 作为真实信号;关闭时显示资料缓存中的对方头像并保留音频,轨道恢复后自动显示视频。</li>
|
||
<li>不需要业务 HTTP。可选 <code>media-state</code> 必须等服务端扩展事件白名单后另行启用,且不持久化、不改变通话结果。</li>
|
||
</ul>
|
||
<h3>8.11 视频与语音通话悬浮状态中.png</h3>
|
||
<figure><img src="images/视频与语音通话悬浮状态中.png" alt="视频与语音通话悬浮状态中"><figcaption>视频与语音通话悬浮状态中</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>最小化不结束 WebRTC 或 WebSocket,会话仍以同一 <code>call_id</code> 运行;点击恢复全屏不发送新 invite。</li>
|
||
<li>系统悬浮窗权限、画中画和安全区域按平台实现。收到 <code>hang-up</code>、Token 失效或媒体会话结束后立即关闭悬浮窗并落正确结果态。</li>
|
||
</ul>
|
||
<h2>9. 企业工作台与办公协同</h2>
|
||
<h3>9.1 工作台.png</h3>
|
||
<figure><img src="images/工作台.png" alt="工作台"><figcaption>工作台</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/workbench</code> 返回 <code>modules[{code,name,icon_file_id,route,unread_count,enabled,permission}]</code>、布局版本和企业配置。</li>
|
||
<li>服务端按当前 Token 的租户、组织角色和功能开关裁剪;前端只接受白名单内部 <code>route</code>,禁止直接执行服务端任意 URL。</li>
|
||
<li>未读数通过 <code>workbench.badge.updated</code> 系统事件增量更新;切租户必须重新加载,不复用另一企业工作台。</li>
|
||
</ul>
|
||
<h3>9.2 部门组织框架.png</h3>
|
||
<figure><img src="images/部门组织框架.png" alt="部门组织框架"><figcaption>部门组织框架</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>根节点:<code>GET /v1/im/organization/departments?parent_id=root&cursor=</code>;子部门同接口传 <code>parent_id</code>;成员 <code>GET /v1/im/organization/departments/{id}/members?cursor=</code>。</li>
|
||
<li>部门返回 <code>department_id,name,parent_id,child_count,member_count,version</code>;成员只返回当前查看人可见字段。</li>
|
||
<li>全局搜索:<code>GET /v1/im/organization/search?q=&types=department,user&cursor=</code>。服务端执行组织范围授权,离职用户标记不可联系或从结果移除。</li>
|
||
</ul>
|
||
<h3>9.3 会议通知-发起人状态.png</h3>
|
||
<figure><img src="images/会议通知-发起人状态.png" alt="会议通知-发起人状态"><figcaption>会议通知-发起人状态</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>创建:<code>POST /v1/im/meetings</code>,Body <code>{"title":"...","start_at":"...","end_at":"...","location":"...","attendee_user_ids":[],"reminder_minutes":[]}</code>。</li>
|
||
<li>详情:<code>GET /v1/im/meetings/{meeting_id}</code> 返回会议、参会人的 <code>response=pending|accepted|declined|tentative</code>、权限和 <code>version</code>。</li>
|
||
<li>更新/取消:<code>PATCH /v1/im/meetings/{id}</code>、<code>POST /v1/im/meetings/{id}/cancel</code>;提醒未响应者 <code>POST /v1/im/meetings/{id}/reminders</code>。所有写操作带幂等键并生成通知和审计。</li>
|
||
</ul>
|
||
<h3>9.4 会议通知-接收人状态.png</h3>
|
||
<figure><img src="images/会议通知-接收人状态.png" alt="会议通知-接收人状态"><figcaption>会议通知-接收人状态</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>详情同 9.3;响应:<code>PUT /v1/im/meetings/{meeting_id}/my-response</code>,Body <code>{"response":"accepted|declined|tentative","version":n}</code>。</li>
|
||
<li>返回我的最终响应和会议版本,并实时通知发起人;重复提交同状态幂等。</li>
|
||
<li>会议过期返回 <code>MEETING_CLOSED</code> 并只读展示;客户端本地日历写入是可选系统动作,不替代服务端响应。</li>
|
||
</ul>
|
||
<h3>9.5 日报.png</h3>
|
||
<figure><img src="images/日报.png" alt="日报"><figcaption>日报</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/reports/daily?scope=mine&from=&to=&status=&cursor=</code> 返回 <code>report_id,report_date,status,draft_updated_at,submitted_at,read_state</code>。</li>
|
||
<li>新建草稿 <code>POST /v1/im/reports/daily</code>,Body <code>{"report_date":"2026-08-11"}</code>;同一用户同一天唯一,重复创建返回原草稿。</li>
|
||
<li><code>permissions.can_create</code> 和可补交日期由服务端返回,前端不自行判断工作日规则。</li>
|
||
</ul>
|
||
<h3>9.6 日报-普通用户页面.png</h3>
|
||
<figure><img src="images/日报-普通用户页面.png" alt="日报-普通用户页面"><figcaption>日报-普通用户页面</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>编辑:<code>PUT /v1/im/reports/daily/{id}/draft</code>,Body <code>{"content":"...","attachments":[],"version":n}</code>;提交 <code>POST /v1/im/reports/daily/{id}/submit</code>。</li>
|
||
<li>服务端返回自动保存版本,客户端用 2~5 秒防抖且串行保存;版本冲突返回 <code>REPORT_VERSION_CONFLICT</code> 并给最新版本。</li>
|
||
<li>提交后按 <code>editable_until</code> 决定是否可修改;附件使用 <code>purpose=daily_report</code> 上传并做权限绑定。</li>
|
||
</ul>
|
||
<h3>9.7 日报-管理员页面.png</h3>
|
||
<figure><img src="images/日报-管理员页面.png" alt="日报-管理员页面"><figcaption>日报-管理员页面</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/reports/daily/statistics?date=&department_id=</code> 返回 <code>expected_count,submitted_count,missing_count</code> 和组织权限范围。</li>
|
||
<li>明细:<code>GET /v1/im/reports/daily?scope=managed&date=&status=submitted|missing&department_id=&cursor=</code>。</li>
|
||
<li>提醒:<code>POST /v1/im/reports/daily/reminders</code>,Body <code>{"date":"...","user_ids":[]}</code>;服务端过滤无管理权限对象并限流,返回逐项结果。</li>
|
||
</ul>
|
||
<h3>9.8 日报-查看详情.png</h3>
|
||
<figure><img src="images/日报-查看详情.png" alt="日报-查看详情"><figcaption>日报-查看详情</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/reports/daily/{report_id}</code> 返回填报人、日期、内容、附件、状态、版本、修改时间和 <code>permissions</code>。</li>
|
||
<li>管理员查看时服务端记录必要的已读状态;附件下载复用第 7 章 ACL。</li>
|
||
<li>撤回或修订使用专用状态和版本,客户端必须显示最新版本与 <code>revised_at</code>,不能混合旧缓存。</li>
|
||
</ul>
|
||
<h3>9.9 月报(1).png</h3>
|
||
<p>!<a href="images/月报(1">月报(1)</a>.png)</p>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>创建/取得当月草稿:<code>POST /v1/im/reports/monthly</code>,Body <code>{"month":"2026-08"}</code>。</li>
|
||
<li>保存:<code>PUT /v1/im/reports/monthly/{id}/draft</code>,Body <code>{"work_summary":"...","next_plan":"...","attachments":[],"version":n}</code>;提交 <code>POST /v1/im/reports/monthly/{id}/submit</code>。</li>
|
||
<li>字段必填、最大长度、提交截止时间由响应 <code>validation</code> 提供;自动保存和冲突处理同 9.6。</li>
|
||
</ul>
|
||
<h3>9.10 月报.png</h3>
|
||
<figure><img src="images/月报.png" alt="月报"><figcaption>月报</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/reports/monthly?scope=mine|managed&from_month=&to_month=&status=&cursor=</code> 返回月份、填报人、状态和时间。</li>
|
||
<li>管理员统计 <code>GET /v1/im/reports/monthly/statistics?month=&department_id=</code>;普通用户响应中不下发团队统计入口权限。</li>
|
||
<li>未提交项进入 9.9,已提交项进入 9.11,路由由返回 <code>action=edit|view</code> 决定。</li>
|
||
</ul>
|
||
<h3>9.11 月报-详情页.png</h3>
|
||
<figure><img src="images/月报-详情页.png" alt="月报-详情页"><figcaption>月报-详情页</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/reports/monthly/{report_id}</code> 返回 <code>month,author,work_summary,next_plan,attachments,status,submitted_at,updated_at,version,permissions</code>。</li>
|
||
<li><code>permissions.can_edit=true</code> 时进入草稿更新;否则只读。修改已提交月报需专用 <code>POST /v1/im/reports/monthly/{id}/reopen</code> 或审核流程,不直接覆盖。</li>
|
||
</ul>
|
||
<h3>9.12 总经理信箱.png</h3>
|
||
<figure><img src="images/总经理信箱.png" alt="总经理信箱"><figcaption>总经理信箱</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>页面配置:<code>GET /v1/im/executive-mailbox/config</code> 返回 <code>enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice</code>。</li>
|
||
<li>提交:<code>POST /v1/im/executive-mailbox/submissions</code>,Body <code>{"subject":"...","content":"...","anonymous":false,"attachment_file_ids":[]}</code>,带幂等键。</li>
|
||
<li>成功仅返回 <code>submission_id,status:"received",submitted_at</code>;匿名模式下业务接收方不见身份,但安全审计是否保留内部追踪按企业政策执行并提前告知。</li>
|
||
</ul>
|
||
<h3>9.13 总经理信箱-文案提示.png</h3>
|
||
<figure><img src="images/总经理信箱-文案提示.png" alt="总经理信箱-文案提示"><figcaption>总经理信箱-文案提示</figcaption></figure>
|
||
<p><strong>状态:新增配置接口。</strong></p>
|
||
<ul>
|
||
<li>文案使用 9.12 配置中的 <code>notice{title,content,updated_at}</code>,由租户后台配置;客户端不得硬编码实名/匿名承诺。</li>
|
||
<li>提交按钮前必须展示当前版本文案;请求可携带 <code>notice_version</code>,服务端发现关键政策已变更时返回 <code>NOTICE_RECONFIRM_REQUIRED</code>。</li>
|
||
</ul>
|
||
<h2>10. 个人资料、通用设置、隐私与青少年模式</h2>
|
||
<h3>10.1 我的.png</h3>
|
||
<figure><img src="images/我的.png" alt="我的"><figcaption>我的</figcaption></figure>
|
||
<p><strong>状态:基础用户资料已有;产品菜单新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/me</code> 返回公开账号资料、实名/青少年模式摘要和资料版本;<code>GET /v1/im/bootstrap</code> 返回个人菜单与权限。</li>
|
||
<li>头像、昵称更新事件到达后刷新头部;收藏和设置入口分别进入第 7、10 章。</li>
|
||
<li>账号 ID 按字符串展示,客户端不得展示内部数据库 ID 或 tenant_id。</li>
|
||
</ul>
|
||
<h3>10.2 个人信息.png</h3>
|
||
<figure><img src="images/个人信息.png" alt="个人信息"><figcaption>个人信息</figcaption></figure>
|
||
<p><strong>状态:Tinode <code>me</code> 资料更新已有基础;目标产品接口需改造。</strong></p>
|
||
<ul>
|
||
<li>查询 <code>GET /v1/im/me/profile</code> 返回 <code>avatar_file_id,nickname,birth_date,signature,gender,region,public_account,version,field_rules</code>。</li>
|
||
<li>更新统一使用 <code>PATCH /v1/im/me/profile</code>,仅提交变更字段并带 <code>version</code>;服务端更新后同步 Tinode <code>me.public</code> 并推送资料事件。</li>
|
||
<li>头像先以 <code>purpose=user_avatar</code> 上传再 PATCH;任何字段的可见性由隐私策略裁剪。</li>
|
||
</ul>
|
||
<h3>10.3 设置昵称.png</h3>
|
||
<figure><img src="images/设置昵称.png" alt="设置昵称"><figcaption>设置昵称</figcaption></figure>
|
||
<p><strong>状态:需改造。</strong></p>
|
||
<ul>
|
||
<li><code>PATCH /v1/im/me/profile</code>,Body <code>{"nickname":"新昵称","version":n}</code>。</li>
|
||
<li>服务端校验 Unicode 长度、全空白、控制字符和敏感内容;返回更新后的 <code>nickname,version,updated_at</code>。</li>
|
||
<li>成功刷新全局资料和联系人快照,但不得覆盖各群昵称。版本冲突先拉最新资料再由用户确认。</li>
|
||
</ul>
|
||
<h3>10.4 出生日期.png</h3>
|
||
<figure><img src="images/出生日期.png" alt="出生日期"><figcaption>出生日期</figcaption></figure>
|
||
<p><strong>状态:新增产品字段。</strong></p>
|
||
<ul>
|
||
<li>保存同 10.3,Body <code>{"birth_date":"2000-01-02","version":n}</code>;仅传 ISO 日期,不传本地化文本或时区时间戳。</li>
|
||
<li>服务端校验不晚于当前租户日期及合理范围,返回 <code>PROFILE_FIELD_INVALID</code>。展示格式由客户端本地化。</li>
|
||
</ul>
|
||
<h3>10.5 个性签名.png</h3>
|
||
<figure><img src="images/个性签名.png" alt="个性签名"><figcaption>个性签名</figcaption></figure>
|
||
<p><strong>状态:需改造。</strong></p>
|
||
<ul>
|
||
<li>保存同 10.3,Body <code>{"signature":"...","version":n}</code>;空字符串表示清空。</li>
|
||
<li>服务端按 <code>field_rules.signature.max_length</code> 和内容审核处理;失败保留本地草稿,成功刷新引用页面。</li>
|
||
</ul>
|
||
<h3>10.6 二维码.png</h3>
|
||
<figure><img src="images/二维码.png" alt="二维码"><figcaption>二维码</figcaption></figure>
|
||
<p><strong>状态:图标纯客户端;二维码协议新增。</strong></p>
|
||
<ul>
|
||
<li>点击入口进入 5.4 并创建短期个人二维码。每次主动刷新调用 <code>POST /v1/im/me/qrcodes</code> 获取新 Token。</li>
|
||
<li>二维码不能使用静态 <code>user_id</code>、手机号或登录 Token;过期、撤销和切租户后必须重新生成。</li>
|
||
</ul>
|
||
<h3>10.7 设置(iOS).png</h3>
|
||
<figure><img src="images/设置(iOS).png" alt="设置(iOS)"><figcaption>设置(iOS)</figcaption></figure>
|
||
<p><strong>状态:菜单纯客户端;数据接口部分新增。</strong></p>
|
||
<ul>
|
||
<li>菜单来自 <code>bootstrap.settings_menu</code>,安全、通知、隐私、通用、关于按权限展示。系统通知权限状态由 iOS API 读取,不由服务端伪造。</li>
|
||
<li>退出:<code>POST /v1/im/auth/logout</code> 撤销当前 refresh/access token 和设备会话,成功或 Token 已失效后关闭 WS、清安全存储和本租户用户缓存。</li>
|
||
<li>多设备退出使用 <code>DELETE /v1/im/security/sessions/{session_id}</code>,需二次验证。</li>
|
||
</ul>
|
||
<h3>10.8 设置(安卓).png</h3>
|
||
<figure><img src="images/设置(安卓).png" alt="设置(安卓)"><figcaption>设置(安卓)</figcaption></figure>
|
||
<p><strong>状态:菜单纯客户端;业务协议同 10.7。</strong></p>
|
||
<ul>
|
||
<li>使用相同 <code>bootstrap.settings_menu</code> 与退出协议;Android 系统权限、通知渠道和应用更新使用平台实现。</li>
|
||
<li>服务端只保存应用内通知偏好与设备 Push Token,不把系统设置页面是否打开当作已授权。</li>
|
||
</ul>
|
||
<h3>10.9 清空缓存的提示.png</h3>
|
||
<figure><img src="images/清空缓存的提示.png" alt="清空缓存的提示"><figcaption>清空缓存的提示</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>确认后只删除缩略图、可重下文件、临时上传下载和搜索缓存;不调用服务端删除消息、收藏或账号接口。</li>
|
||
<li>不删除安全存储中的登录凭证,除非用户选择退出登录。完成后重新计算本地占用并显示结果。</li>
|
||
</ul>
|
||
<h3>10.10 版本更新页面.png</h3>
|
||
<figure><img src="images/版本更新页面.png" alt="版本更新页面"><figcaption>版本更新页面</figcaption></figure>
|
||
<p><strong>状态:新增 App 配置接口。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/app/releases/latest?platform=ios|android&current_version=1.0.0&channel=stable</code>,未登录可在企业解析后调用。</li>
|
||
<li>返回 <code>latest_version,min_supported_version,update_type=none|optional|required,release_notes,store_url,package_url,checksum,published_at</code>。</li>
|
||
<li>客户端按平台只打开受信任商店/签名下载地址;Android 包必须校验签名和 checksum,服务端 URL 使用允许域名。</li>
|
||
</ul>
|
||
<h3>10.11 发现新版本.png</h3>
|
||
<figure><img src="images/发现新版本.png" alt="发现新版本"><figcaption>发现新版本</figcaption></figure>
|
||
<p><strong>状态:纯客户端弹窗;数据来自 10.10。</strong></p>
|
||
<ul>
|
||
<li><code>optional</code> 显示稍后/更新,记录本地已忽略版本并按服务端 <code>remind_after</code> 再提示;<code>required</code> 不显示绕过入口。</li>
|
||
<li>下载和安装状态由平台管理;接口异常时不得误判为强制更新,除非本地已有有效签名的最低版本策略缓存。</li>
|
||
</ul>
|
||
<h3>10.12 隐私设置.png</h3>
|
||
<figure><img src="images/隐私设置.png" alt="隐私设置"><figcaption>隐私设置</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/me/privacy</code> 返回 <code>add_me_methods,profile_visibility,last_seen_visibility,read_receipts,version</code> 和各字段 <code>editable</code>。</li>
|
||
<li><code>PATCH /v1/im/me/privacy</code> 只提交变更字段和 <code>version</code>;成功返回完整最新配置并向其他设备推送 <code>privacy.updated</code>。</li>
|
||
<li>黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。</li>
|
||
</ul>
|
||
<h3>10.13 隐私设置-添加我的方式选项.png</h3>
|
||
<figure><img src="images/隐私设置-添加我的方式选项.png" alt="隐私设置-添加我的方式选项"><figcaption>隐私设置-添加我的方式选项</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>add_me_methods</code> 固定字段 <code>account,phone,qr,group</code>,每项为布尔值;更新用 10.12 PATCH,如 <code>{"add_me_methods":{"phone":false},"version":n}</code>。</li>
|
||
<li>关闭后对应搜索/二维码/群来源申请由服务端统一返回 <code>FRIEND_REQUEST_NOT_ALLOWED</code> 或 <code>USER_NOT_DISCOVERABLE</code>,不泄露具体关闭项。</li>
|
||
<li>企业强制策略可令字段 <code>editable=false</code>,前端禁用并显示服务端提供的通用说明。</li>
|
||
</ul>
|
||
<h3>10.14 青少年模式未开启(安卓).png</h3>
|
||
<figure><img src="images/青少年模式未开启(安卓).png" alt="青少年模式未开启(安卓)"><figcaption>青少年模式未开启(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li><code>GET /v1/im/settings/youth-mode</code> 返回 <code>enabled,policy_version,restrictions,password_set,can_enable</code>。</li>
|
||
<li>点击开启先展示 10.15,再按 1.7 完成身份验证和 1.11 设置独立密码;最后调用 <code>POST /v1/im/settings/youth-mode/enable</code>,Body <code>{"policy_version":n,"action_token":"..."}</code>。</li>
|
||
<li>服务端持久化到用户租户范围并推送所有设备,不能仅设置本地标志。</li>
|
||
</ul>
|
||
<h3>10.15 青少年模式提示(安卓).png</h3>
|
||
<figure><img src="images/青少年模式提示(安卓).png" alt="青少年模式提示(安卓)"><figcaption>青少年模式提示(安卓)</figcaption></figure>
|
||
<p><strong>状态:弹窗纯客户端;文案由服务端配置。</strong></p>
|
||
<ul>
|
||
<li>使用 10.14 返回的 <code>restrictions</code> 与 <code>policy_version</code> 渲染;取消不请求,确认进入密码设置/验证。</li>
|
||
<li>最终 enable 请求携带用户确认的 <code>policy_version</code>;若策略更新返回 <code>POLICY_RECONFIRM_REQUIRED</code>,重新展示新内容。</li>
|
||
</ul>
|
||
<h3>10.16 青少年模式已开启(安卓).png</h3>
|
||
<figure><img src="images/青少年模式已开启(安卓).png" alt="青少年模式已开启(安卓)"><figcaption>青少年模式已开启(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>页面调用 10.14,<code>enabled=true</code> 时展示 <code>enabled_at,restrictions</code>。<code>bootstrap</code> 同时返回受限模块和动作。</li>
|
||
<li>受限接口服务端返回 <code>YOUTH_MODE_RESTRICTED</code>;前端隐藏只是体验优化,不能作为安全控制。</li>
|
||
<li>登录、App 重启和另一设备均从服务端恢复状态;收到 <code>youth_mode.updated</code> 即刷新。</li>
|
||
</ul>
|
||
<h3>10.17 关闭青少年模式(安卓).png</h3>
|
||
<figure><img src="images/关闭青少年模式(安卓).png" alt="关闭青少年模式(安卓)"><figcaption>关闭青少年模式(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>先调用 1.11 的独立密码验证,获得 <code>action_token</code>;再 <code>POST /v1/im/settings/youth-mode/disable</code>,Header <code>X-Action-Token</code>,带幂等键。</li>
|
||
<li>成功返回 <code>enabled:false,updated_at</code> 并广播;连续密码错误按服务端 <code>retry_after_seconds</code> 锁定。</li>
|
||
<li>忘记密码走 1.7 身份验证后重置,不能绕过密码直接修改本地状态。</li>
|
||
</ul>
|
||
<h3>10.18 获取推送权限(安卓).png</h3>
|
||
<figure><img src="images/获取推送权限(安卓).png" alt="获取推送权限(安卓)"><figcaption>获取推送权限(安卓)</figcaption></figure>
|
||
<p><strong>状态:权限引导纯客户端;设备注册需改造。</strong></p>
|
||
<ul>
|
||
<li>用户确认后调用 Android 系统权限;拒绝状态本地记录,最终以系统回调为准。</li>
|
||
<li>获取 Push Token 后 <code>PUT /v1/im/devices/{device_id}/push-token</code>,Body <code>{"provider":"fcm","token":"...","app_version":"..."}</code>;Token 轮换时覆盖。</li>
|
||
<li>退出登录删除/解绑该用户设备 Token。Push Token 属敏感凭证,不写日志。</li>
|
||
</ul>
|
||
<h3>10.19 提醒(安卓).png</h3>
|
||
<figure><img src="images/提醒(安卓).png" alt="提醒(安卓)"><figcaption>提醒(安卓)</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>仅在功能因系统权限不可用时显示;取消和“前往设置”不调用业务服务端。</li>
|
||
<li>回到应用后重新读取真实权限,不能因用户打开过设置页就假定授权成功。</li>
|
||
</ul>
|
||
<h3>10.20 相机说明(安卓).png</h3>
|
||
<figure><img src="images/相机说明(安卓).png" alt="相机说明(安卓)"><figcaption>相机说明(安卓)</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>首次拍照、扫码或视频通话前展示用途说明,确认后再触发系统权限;拒绝不发媒体/扫码请求。</li>
|
||
<li>永久拒绝时提供系统设置入口;允许从相册选择等替代能力。客户端不得上传系统权限状态作为服务端授权依据。</li>
|
||
</ul>
|
||
<h3>10.21 麦克风说明(安卓).png</h3>
|
||
<figure><img src="images/麦克风说明(安卓).png" alt="麦克风说明(安卓)"><figcaption>麦克风说明(安卓)</figcaption></figure>
|
||
<p><strong>状态:纯客户端。</strong></p>
|
||
<ul>
|
||
<li>首次录音或通话前说明并申请系统权限;未授权时不得创建录音文件或发送 call invite。</li>
|
||
<li>永久拒绝后进入系统设置引导;恢复授权后重新执行 4.7 或第 8 章动作,不复用过期媒体会话。</li>
|
||
</ul>
|
||
<h2>11. 反诈安全、举报与意见反馈</h2>
|
||
<h3>11.1 反诈弹窗(安卓).png</h3>
|
||
<figure><img src="images/反诈弹窗(安卓).png" alt="反诈弹窗(安卓)"><figcaption>反诈弹窗(安卓)</figcaption></figure>
|
||
<p><strong>状态:新增风控决策协议。</strong></p>
|
||
<ul>
|
||
<li>高风险动作在原业务接口返回 <code>409 RISK_CONFIRMATION_REQUIRED</code>,<code>details={risk_challenge_id,level,notice_code,notice_params,expires_at,action}</code>。</li>
|
||
<li>客户端用 <code>notice_code</code> 映射经过审核的本地文案,不显示服务端规则、分数或命中特征;取消不重试原操作。</li>
|
||
<li>低风险提示也可由可信 <code>security.risk_notice</code> 系统事件触发。普通消息不得伪装成系统反诈弹窗。</li>
|
||
</ul>
|
||
<h3>11.2 反诈弹窗(二次提示安卓).png</h3>
|
||
<figure><img src="images/反诈弹窗(二次提示安卓).png" alt="反诈弹窗(二次提示安卓)"><figcaption>反诈弹窗(二次提示安卓)</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>确认:<code>POST /v1/im/security/risk-challenges/{risk_challenge_id}/confirm</code>,Body <code>{"decision":"continue|cancel"}</code>,带幂等键。</li>
|
||
<li><code>continue</code> 成功返回单次、短期 <code>risk_action_token</code>;客户端在重试原业务接口时以 <code>X-Risk-Action-Token</code> 携带,原请求幂等键保持不变。</li>
|
||
<li>服务端可返回 <code>RISK_ACTION_BLOCKED</code> 直接阻断;确认、取消、过期均写审计,客户端不得无限创建 challenge 绕过。</li>
|
||
</ul>
|
||
<h3>11.3 举报.png</h3>
|
||
<figure><img src="images/举报.png" alt="举报"><figcaption>举报</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>原因:<code>GET /v1/im/reports/abuse/reasons?target_type=user|group|message</code> 返回 <code>reason_code,label,requires_description,allows_evidence</code>。</li>
|
||
<li>进入页面携带受控目标:用户 <code>user_id</code>、群 <code>topic</code> 或消息 <code>(topic,seq)</code>;客户端不提交 tenant_id 或被举报者任意文本身份。</li>
|
||
<li>服务端先验证举报人能够看到目标,再返回适用原因;举报人身份不下发给被举报方。</li>
|
||
</ul>
|
||
<h3>11.4 举报(1).png</h3>
|
||
<p>!<a href="images/举报(1">举报(1)</a>.png)</p>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>证据使用 <code>purpose=abuse_evidence</code> 上传;提交 <code>POST /v1/im/reports/abuse</code>,Body <code>{"target":{"type":"message","topic":"...","seq":"12"},"reason_code":"fraud","description":"...","evidence_file_ids":[]}</code>。</li>
|
||
<li>服务端从目标消息保存必要、受控的审核快照,校验证据数量/MIME/大小,返回 <code>report_id,status:"received"</code>。</li>
|
||
<li>重复举报按策略合并并返回原受理号或 <code>REPORT_RATE_LIMITED</code>;证据使用独立严格 ACL 和保留期。</li>
|
||
</ul>
|
||
<h3>11.5 意见反馈.png</h3>
|
||
<figure><img src="images/意见反馈.png" alt="意见反馈"><figcaption>意见反馈</figcaption></figure>
|
||
<p><strong>状态:新增。</strong></p>
|
||
<ul>
|
||
<li>配置:<code>GET /v1/im/feedback/config</code> 返回类别、最大正文、附件规则、是否允许联系方式和日志授权说明。</li>
|
||
<li>提交:<code>POST /v1/im/feedback</code>,Body <code>{"category":"bug","content":"...","contact":"...","attachment_file_ids":[],"client_context":{"platform":"android","app_version":"1.0.0","os_version":"..."},"include_diagnostics":false}</code>。</li>
|
||
<li>附件用 <code>purpose=feedback</code>;只有用户明确同意 <code>include_diagnostics=true</code> 才上传经过脱敏的诊断包。成功返回 <code>feedback_id,status:"received"</code>,重复提交由幂等键去重。</li>
|
||
</ul>
|
||
<h2>12. 联调实施清单</h2>
|
||
<h3>12.1 当前可直接联调的基础能力</h3>
|
||
<ol>
|
||
<li>WebSocket <code>/v0/channels</code>、Wire Version <code>0.25</code> 和最低版本 <code>0.20</code>。</li>
|
||
<li>第一条 <code>hi</code> 携带企业码并固定 Session 租户;响应包含公开租户信息、限制、ICE 配置和呼叫超时。</li>
|
||
<li><code>basic</code>/<code>token</code> 登录,<code>sub/get/set/pub/leave/del/note</code> 基础 Tinode 协议。</li>
|
||
<li><code>pub</code> 成功返回 <code>202 + seq</code>,Topic 消息与基础回执同步。</li>
|
||
<li><code>/v0/file/u/</code> 和 <code>/v0/file/s/{file_id}</code> 的基础上传下载及租户存储隔离。</li>
|
||
</ol>
|
||
<h3>12.2 必须优先完成的服务端阻塞项</h3>
|
||
<table><thead><tr><th>优先级</th><th>改造项</th><th>阻塞图片章节</th></tr></thead><tbody>
|
||
<tr><td>P0</td><td><code>/v1/im</code> 统一认证中间件、响应封装、幂等键、租户 Principal</td><td>全部 HTTP 产品能力</td></tr>
|
||
<tr><td>P0</td><td>企业码解析、验证码、密码重置、二次认证、Token <code>auth_version</code> 和刷新/撤销</td><td>1.1~1.11、10.7~10.8</td></tr>
|
||
<tr><td>P0</td><td>普通用户禁止发布系统 Topic;系统事件可信生产通道</td><td>2.5、3.1~3.5、6.4、11.1</td></tr>
|
||
<tr><td>P0</td><td>消息 <code>x-im-schema/type/client-mid</code> 校验和服务端幂等</td><td>2~4 章</td></tr>
|
||
<tr><td>P0</td><td>文件资源级 ACL、Range、ETag、用途绑定和租户存储配额</td><td>3.6、4.10~4.14、6.27~7.16、11.4~11.5</td></tr>
|
||
<tr><td>P1</td><td>好友申请、好友关系、黑名单状态机</td><td>第 3、5 章</td></tr>
|
||
<tr><td>P1</td><td>群成员、角色、禁言、公告、邀请审批和配额</td><td>第 6 章</td></tr>
|
||
<tr><td>P1</td><td>收藏、转发和聊天记录搜索</td><td>第 4、7 章</td></tr>
|
||
<tr><td>P1</td><td>通话媒体状态、结果消息、Push 来电</td><td>第 8 章</td></tr>
|
||
<tr><td>P2</td><td>工作台、组织、会议、日报、月报、信箱</td><td>第 9 章</td></tr>
|
||
<tr><td>P2</td><td>隐私、青少年模式、反诈、举报、反馈</td><td>第 10、11 章</td></tr>
|
||
</tbody></table>
|
||
<h3>12.3 前后端联调最低验收用例</h3>
|
||
<table><thead><tr><th>编号</th><th>场景</th><th>预期结果</th></tr></thead><tbody>
|
||
<tr><td>T-01</td><td>A 企业 Token 连接 B 企业 Session</td><td>登录失败 <code>TENANT_MISMATCH</code>,不产生用户/Topic 数据</td></tr>
|
||
<tr><td>T-02</td><td>企业停用后使用旧 Token 登录和请求 HTTP</td><td>WS/HTTP 均拒绝,客户端清除凭证并回企业入口</td></tr>
|
||
<tr><td>T-03</td><td>同 <code>client_mid</code> 断线重发同一消息</td><td>只落一条消息并返回同一 <code>seq</code></td></tr>
|
||
<tr><td>T-04</td><td>相同 HTTP 幂等键、相同请求体重复提交</td><td>返回第一次结果;不同请求体返回 <code>IDEMPOTENCY_CONFLICT</code></td></tr>
|
||
<tr><td>T-05</td><td>被拉黑或删除好友后绕过按钮直接 <code>pub</code></td><td>服务端拒绝,对方不收到消息</td></tr>
|
||
<tr><td>T-06</td><td>普通用户构造 <code>system_event</code> 或向系统 Topic 发布</td><td>服务端拒绝,系统消息页不展示可信样式</td></tr>
|
||
<tr><td>T-07</td><td>普通群成员调用管理员、禁言、转让群主接口</td><td>返回 <code>FORBIDDEN</code>,群状态不变且写安全审计</td></tr>
|
||
<tr><td>T-08</td><td>群成员数已达租户配置上限时同意两个并发申请</td><td>至多一个成功,成员数不超限</td></tr>
|
||
<tr><td>T-09</td><td>非消息接收者持同租户 Token 下载文件</td><td>返回 <code>FILE_FORBIDDEN</code>,不能只通过 file_id 下载</td></tr>
|
||
<tr><td>T-10</td><td>文件下载中 Token 过期后刷新并 Range 续传</td><td>文件完整、checksum 正确、不重复下载已完成分片</td></tr>
|
||
<tr><td>T-11</td><td>风控二次确认后重复重放 risk token</td><td>只有首次有效,其余返回 Token 已消费</td></tr>
|
||
<tr><td>T-12</td><td>切换企业后查看缓存、上传任务、收藏和搜索</td><td>不出现上一租户数据或任务</td></tr>
|
||
</tbody></table>
|
||
<h3>12.4 统一错误 reason</h3>
|
||
<p>首版客户端至少实现:<code>TENANT_UNAVAILABLE</code>、<code>TENANT_MISMATCH</code>、<code>AUTH_FAILED</code>、<code>TOKEN_EXPIRED</code>、<code>ACCOUNT_DISABLED</code>、<code>RATE_LIMITED</code>、<code>VALIDATION_FAILED</code>、<code>IDEMPOTENCY_CONFLICT</code>、<code>RESOURCE_NOT_FOUND</code>、<code>FORBIDDEN</code>、<code>FRIEND_RELATION_REQUIRED</code>、<code>MESSAGE_NOT_ALLOWED</code>、<code>GROUP_MUTED</code>、<code>GROUP_MEMBER_LIMIT_EXCEEDED</code>、<code>TENANT_USER_QUOTA_EXCEEDED</code>、<code>TENANT_GROUP_QUOTA_EXCEEDED</code>、<code>FILE_TOO_LARGE</code>、<code>FILE_FORBIDDEN</code>、<code>FILE_UNAVAILABLE</code>、<code>STORAGE_QUOTA_EXCEEDED</code>、<code>REALNAME_REQUIRED</code>、<code>YOUTH_MODE_RESTRICTED</code>、<code>RISK_CONFIRMATION_REQUIRED</code>、<code>RISK_ACTION_BLOCKED</code>。</p>
|
||
<p>未知 <code>reason</code> 使用通用失败文案并记录 <code>request_id</code>,不得把服务端 <code>message</code> 原样展示给用户。</p>
|
||
<h3>12.5 完成定义</h3>
|
||
<p>每个图片小节进入联调前,服务端须提供 OpenAPI/JSON Schema、错误码、权限规则和可用测试租户;前端须提供请求日志脱敏、断线/重试行为和四态页面(加载、空、成功、失败)。联调完成必须逐图勾选对应协议、至少覆盖成功、权限拒绝、重复提交、断网恢复和跨租户五类场景。</p>
|
||
</body></html> |