@@ -212,6 +212,18 @@ WebSocket Upgrade 阶段由 `server/hdl_websock.go` 先调用 `checkAPIKey`。
订阅成功后才允许 `{pub}` 、`note read/recv` 等会话操作。断线恢复必须重新执行 `WebSocket Upgrade -> hi -> token login -> sub me -> 恢复 Topic` ,同一连接内不得跳过 `hi` 或切换企业码。
##### 0.5.1.1 WebSocket 心跳与在线状态
当前 IM 长连接心跳使用 WebSocket 协议层 Ping/Pong,不使用业务 JSON 消息做保活。服务端每 30 秒发送一次 WebSocket Ping 帧(`pingPeriod=30s` ),并期望客户端在 45 秒读超时窗口内返回 Pong(`idleSessionTimeout=45s` );如果没有收到 Pong,或底层读写失败,服务端会关闭当前 Session。
客户端要求:
1. 使用系统 WebSocket 库时,必须确认库会自动回复 Pong;如果库不会自动回复,需要在收到 Ping 时显式发送 Pong。
2. 不要把 `{"note":{"what":"kp"}}` 当作心跳。`kp/kpa/kpv` 只是“正在输入/正在录音/正在视频输入”等 UI 状态通知,不落库,也不能替代 WebSocket Pong。
3. 连接关闭或报错后,客户端本地应立即把 IM 长连接视为离线,并按指数退避重连。重连成功后完整执行 `WebSocket Upgrade -> hi -> token login -> sub me -> sub topics -> get data/del` ,用本地最后完整持久化的 `seq + 1` 增量补消息。
4. 服务端收到任意客户端上行 JSON 消息或 WebSocket Pong 后,都会刷新在线状态。当前在线状态写入 Redis,默认 TTL 为 120 秒,用于好友在线状态、群成员在线状态等展示。
5. iOS/Android 进入后台后系统可能挂起 WebSocket。后台场景以推送唤醒和前台重连补偿为主,不要求客户端在后台无限重连。
#### 0.5.2 Long Polling 兼容流程:先取得的是 sid
如果客户端无法使用 WebSocket,可以使用现有 `/v0/channels/lp` 。该模式确实需要“先请求取得一个值”,但取得的是短期 Session ID `sid` ,API Key 仍然必须由客户端预先持有。
@@ -357,6 +369,8 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| 删除单条/多条消息 | `{"del":{"id":"del-1","topic":"grp...","what":"msg","delseq":[{"low":12,"hi":13}],"hard":false}}` | `{"ctrl":{"id":"del-1","code":200,"text":"ok","params":{"del":3}}}` ;其他端可能收到 `{"pres":{"topic":"grp...","what":"del","seq":3}}` |
| 清空当前用户视图 | `{"del":{"id":"del-all-1","topic":"p2p...","what":"msg","delseq":[{"low":1,"hi":126}],"hard":false}}` | `{"ctrl":{"id":"del-all-1","code":200,"text":"ok","params":{"del":4}}}` |
`kp/kpa/kpv` 只表示输入态、录音态、视频输入态等短暂 UI 状态,不能作为长连接心跳。长连接保活必须依赖 0.5.1.1 中的 WebSocket Ping/Pong。
##### 0.5.4.5 Topic 资料、个人群设置和标签
| 场景 | 客户端发送 | 服务端返回/下行示例 |
@@ -590,7 +604,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| WebSocket `acc new` | `{"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"alice","phone":"15500000000","secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"15500000000","resp":"verification_token","params":{"challenge_id":"chg_123"}}]}}` | `{"ctrl":{"id":"acc-1","code":200,"params":{"user":"usr...","username":"alice","token":"token...","expires":"2026-09-03T09:00:00Z"}}}` |
| `POST /v0/channels/lp` | Long Polling 首包 `{"hi":{"id":"hi-1","ver":"0.25","tenant":"default"}}` | `{"ctrl":{"id":"hi-1","code":201,"params":{"sid":"lp_sid_123"}}}` |
| `POST /v0/file/u/` | Multipart 文件;Header `X-MeetingIM-Auth: token <base64_token>` ;业务表单可带 `purpose=chat_file&topic=grp...` | `{"ctrl":{"code":200,"params":{"url":"/v0/file/s/file123","name":"a.pdf","size":1024,"mime":"application/pdf"}}}` |
| `GET /v0/file/s/{file_id}` | Header `X-MeetingIM-Auth: token <base64_token>` ,可带 `Range: bytes=0-` | 返回文件字节流;失败返回 `FILE_FORBIDDEN` 或 `FILE_UNAVAILABLE` |
| `GET /v0/file/s/{file_id}` | Header `X-MeetingIM-Auth: token <base64_token>` ,可带 `Range: bytes=0-` | 对象存储模式下鉴权通过后返回 302 到短期签名 URL;客户端跟随 Location 获取文件 |
### 0.6 Topic 与产品消息
@@ -1396,7 +1410,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:新增产品通讯录接口。 **
- 好友列表:`GET /v1/im/friends?query=&initial=&cursor=` ,返回 `user_id,display_name,remark,avatar_file_id,initial,online_visibility` ; 显示名优先级为备注名、昵称、账号 。
- 好友列表:`GET /v1/im/friends?query=&initial=&cursor=` ,返回 `user_id,display_name,remark,avatar_file_id,initial,online,online _visibility` ; 好友展示名统一读取 `display_name` ,服务端按当前登录人计算,优先级为备注名、昵称、账号。 `remark` 仅用于备注编辑页回显, `nickname` 表示对方自己设置的昵称,不应直接作为好友列表或单聊标题的最终展示名 。
- 分页:`next_cursor` 当前按最后一条好友的 `peer_id` 生成,客户端必须作为不透明字符串原样回传,不要自行解析、排序或拼接。
- 组织成员:`GET /v1/im/organization/members?query=&department_id=&cursor=` ;好友和组织成员是两种关系,前端不得混为一个可发消息权限。
- 新朋友、群列表、文件助手和系统消息入口由 `GET /v1/im/bootstrap` 的 `entries` 控制显示,初始红点由 `entry_badges` 和各入口 `unread_count` 返回。新朋友待处理数使用业务统计;文件助手当前返回 `topic:"slf", unread_source:"topic"` ;系统消息当前返回 `topic:"sys", unread_source:"business"` ,未读来自系统通知业务表。列表缓存以租户和用户隔离。
@@ -1458,7 +1472,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:新增。 **
- `GET /v1/im/users/{identifier}/profile` 返回按当前查看人裁剪后的 `display_name,remark,avatar_file_id,gender,region,public_account,signature,relationship,capabilities` ; `identifier` 可传 `user_id` 、`username` 或 `public_account` 。
- `GET /v1/im/users/{identifier}/profile` 返回按当前查看人裁剪后的 `display_name,remark,avatar_file_id,gender,region,public_account,signature,relationship,capabilities` ; `identifier` 可传 `user_id` 、`username` 或 `public_account` 。个人资料页头部和单聊标题统一使用 `display_name` ,需要展示对方原始昵称时再使用 `nickname` 。
- 查看该用户二维码调用 `POST /v1/im/users/{identifier}/qrcodes` , Body `{"purpose":"add_friend","expires_in_seconds":86400}` ;前端在个人信息页只有用户名时直接把用户名作为 `identifier` 传入即可。服务端解析到真实用户后生成二维码,并按资料可见性与 `add_me_methods.qr` 做权限校验。
- 发消息使用返回的 P2P `topic` ;没有 Topic 时调用 `POST /v1/im/conversations/p2p` , Body `{"peer_user_id":"..."}` 并由服务端验证好友关系。
- 音视频按钮只按 `capabilities.can_call` 展示,真正呼叫时再次鉴权。隐藏字段必须从响应中省略,不能只由前端遮挡。
@@ -2218,10 +2232,10 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:新增。 **
- `GET /v1/im/workbench` 返回 `modules[{code,name,icon_file_id,route,unread_count,enabled,permission}]` 、布局版本和企业配置。
- `GET /v1/im/bootstrap` 返回 `entry_badges.workbench_total_unread` ,用于工作台一级入口总红点;总数 只汇总当前用户可见且 `enabled=true` 的工作台模块 。
- `GET /v1/im/workbench` 返回 `modules[{code,name,icon_file_id,route,sort_order, unread_count,enabled,permission}]` 、布局版本和企业配置;服务端按 `sort_order ASC` 返回,默认顺序为打卡、月报、日报、部门框架、举报信箱、会议通知、会议笔记 。
- `GET /v1/im/bootstrap` 返回 `entry_badges.workbench_total_unread` ,用于工作台一级入口总红点;当前 只汇总“会议通知”模块的未读通知,会议笔记、日报、月报、打卡、部门框架和举报信箱均不产生工作台通知数 。
- 服务端按当前 Token 的租户、组织角色和功能开关裁剪;前端只接受白名单内部 `route` ,禁止直接执行服务端任意 URL。
- 工作台一级总未读以 `GET /v1/im/bootstrap` 和 `GET /v1/im/workbench` 返回为准;若后续启用 `workbench.badge.updated` ,应按 0.5.4.6 的 `info.what="im"` 业务事件下发。切租户必须重新加载,不复用另一企业工作台。
- 工作台一级总未读以 `GET /v1/im/bootstrap` 和 `GET /v1/im/workbench` 返回为准;当前只有会议通知可接收移动端通知,其他工作台模块在进入页面时主动拉取数据刷新。 若后续启用 `workbench.badge.updated` ,应按 0.5.4.6 的 `info.what="im"` 业务事件下发。切租户必须重新加载,不复用另一企业工作台。
### 9.2 部门组织框架.png
@@ -2251,7 +2265,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:新增。 **
- 创建:`POST /v1/im/meetings` , Body `{"title":"...","start_at":"...","end_at":"...","location":"...","attendee_user_ids":[],"reminder_minutes":[]}` 。
- 创建:`POST /v1/im/meetings` , Body `{"title":"...","start_at":"...","end_at":"...","location":"...","recorder_user_id":"usr_me"," attendee_user_ids":[],"reminder_minutes":[]}` ; `recorder_user_id` 未传时服务端默认使用当前发起人 。
- 详情:`GET /v1/im/meetings/{meeting_id}` 返回会议、参会人的 `response=pending|accepted|declined|tentative` 、权限和 `version` 。
- 更新/取消:`PATCH /v1/im/meetings/{id}` 、`POST /v1/im/meetings/{id}/cancel` ;提醒未响应者 `POST /v1/im/meetings/{id}/reminders` 。所有写操作带幂等键并生成通知和审计。
- 主动下行:参会人收到 `info.what="im"` ,事件为 `meeting.created/updated/canceled/reminded/response.updated` , payload 包含 `meeting_id,version,state` ;前端收到后刷新会议列表或会议详情。除操作者外,当前会写入系统通知列表。
@@ -2364,7 +2378,174 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
- 文案使用 9.12 配置中的 `notice{title,content,updated_at}` ,由租户后台配置;客户端不得硬编码实名/匿名承诺。
- 提交按钮前必须展示当前版本文案;请求可携带 `notice_version` ,服务端发现关键政策已变更时返回 `NOTICE_RECONFIRM_REQUIRED` 。
### 9.14 接口示例
### 9.14 会议笔记
会议笔记属于企业工作台模块,复用会议主数据和统一文件能力。服务端负责判断会议是否结束、当前用户是否与会议有关、是否为会议记录员,以及文件查看和操作权限;客户端只根据响应中的状态与 `permissions` 展示页面,不自行推导权限。
本模块遵循主文档第 0 章的公共约定:产品 HTTP 使用 `X-MeetingIM-APIKey` 、`X-IM-Schema: 1` 、`X-Request-ID` 和 `Authorization: Bearer <token>` ;写操作额外使用 `Idempotency-Key` ;成功和失败响应均使用统一包裹结构。
会议笔记不产生移动端通知、不写入会议通知未读数,也不增加工作台底部 Tab 红点。客户端进入会议笔记列表或详情页时主动请求最新数据;从上传、预览或下载页面返回后,只刷新当前会议笔记页面。
### 9.14.1 会议笔记-工作台入口.png
<img src="images/会议笔记-工作台入口.png" alt="会议笔记-工作台入口" width="360">
**状态:工作台接口需改造。 **
- `GET /v1/im/workbench` 的 `modules` 增加 `code="meeting_notes"` 、`route="/meeting-notes"` 。服务端按租户功能开关和当前用户权限决定是否返回该入口。
- 会议笔记模块固定返回 `unread_count=0` ,不参与 `GET /v1/im/bootstrap` 的 `entry_badges.workbench_total_unread` 汇总。
- 当前工作台红点只对应“会议通知”模块的未读通知。日报、月报、打卡、部门框架、举报信箱和会议笔记均不产生工作台通知数。
- 客户端点击入口后请求 `GET /v1/im/meeting-notes` 。切换企业后必须清空原租户的会议笔记缓存并重新加载。
### 9.14.2 会议笔记-列表.png
<img src="images/会议笔记-列表.png" alt="会议笔记-列表" width="360">
**状态:新增。 **
- 列表:`GET /v1/im/meeting-notes?query=&cursor=&limit=20` 。接口只返回当前用户作为发起人、会议记录员或参会人的已结束会议,不返回未开始、进行中或已取消会议。
- `query` 支持匹配会议主题、发起人、会议记录员、参会人和地点;搜索范围仍受当前用户会议可见权限限制。
- 列表按 `end_at` 倒序排列,分页使用不透明 `cursor` 。客户端不得用本地时间过滤会议状态或自行扩大搜索范围。
- 已结束但尚未上传笔记的会议仍返回,使用 `note_status="empty"` 、`file_count=0` ;客户端展示会议基础信息,进入详情后显示“暂无笔记”。
- `permissions.can_view=true` 决定是否允许进入详情;`permissions.can_upload` 只对当前会议记录员为 `true` 。普通参会人可以进入详情,但不显示上传按钮。
- `state="ended"` 是服务端按会议状态及服务端当前时间计算或持久化的有效状态。服务端必须以 `end_at` 和取消状态为准,不能信任客户端传入的“已结束”条件。
### 9.14.3 会议笔记-发布页面.png
<img src="images/会议笔记-发布页面.png" alt="会议笔记-发布页面" width="360">
**状态:现有会议创建接口需改造。 **
- 页面字段顺序为发起人、地点、会议记录员、参会人、开始时间、时长、会议主题。会议记录员默认显示当前会议发起人。
- 创建仍使用 `POST /v1/im/meetings` ,请求新增 `recorder_user_id` 。客户端未传或传空字符串时,服务端默认使用当前登录用户;客户端不能通过 `organizer_id` 伪造发起人。
- 服务端校验会议记录员属于当前租户且账号有效。首版允许会议记录员是发起人或参会人;若选择了非参会人,服务端自动加入参会人列表并在响应中返回最终名单。
- 服务端保存的会议必须有 `recorder_user_id` ;创建请求可省略该字段以使用发起人默认值。`attendee_user_ids` 、`start_at` 、`end_at` 和 `title` 为必填业务数据,`location` 可为空,且 `end_at` 必须晚于 `start_at` 。
- 创建成功后的会议通知仍统一归属“会议通知”模块;会议笔记模块不额外生成通知或未读数。
- `PATCH /v1/im/meetings/{meeting_id}` 支持在会议结束前修改 `recorder_user_id` ,并使用 `version` 做乐观锁。会议结束后返回 `MEETING_CLOSED` 。
### 9.14.4 会议笔记-详情页.png
<img src="images/会议笔记-详情页.png" alt="会议笔记-详情页" width="360">
**状态:新增。 **
- 详情:`GET /v1/im/meetings/{meeting_id}/notes` ,一次返回会议基础信息、发起人、会议记录员、参会人、文件列表、笔记状态和当前用户权限。
- 会议基础信息与会议通知页面共用 `im_meetings` 和 `im_meeting_participants` 数据,不复制会议标题、时间和成员快照作为另一份可编辑主数据。
- 当前用户为会议记录员且会议已结束时,`permissions.can_upload=true` ,客户端展示“上传文件”;其他用户不展示该按钮。
- 上传文件先调用 `POST /v0/file/u/` , Multipart 携带 `purpose=meeting_note` 和 `meeting_id` ;上传成功取得 `file_id` 后,再调用 `POST /v1/im/meetings/{meeting_id}/notes/files` 建立会议笔记文件记录。
- 文件登记成功后才进入详情列表。上传成功但登记失败的临时文件不得展示,并由服务端按临时文件 TTL 回收。
- 当前会议记录员可替换或删除自己登记的文件;所有写操作均需 `Idempotency-Key` ,服务端记录操作审计。
### 9.14.5 会议笔记-详情页(普通成员).png
<img src="images/会议笔记-详情页(普通成员).png" alt="会议笔记-详情页(普通成员)" width="360">
**状态:新增权限响应。 **
- 普通参会人调用与 9.14.4 相同的详情接口。响应中 `permissions.can_view=true` 、`can_upload=false` 、`can_replace=false` 、`can_delete=false` 。
- 没有已上传文件时返回 `note_status="empty"` 和空数组 `files=[]` ,客户端展示“暂无笔记”,不把空状态当作请求失败。
- 有文件时普通参会人只展示文件名、大小、类型、上传人、上传时间和预览入口,不展示上传、替换、删除按钮。
- 服务端必须在文件登记、替换、删除、预览和下载接口逐次校验会议关系及角色。隐藏客户端按钮不能代替服务端鉴权。
- 与会议无关、已离开可见范围或跨租户的用户统一返回 `RESOURCE_NOT_FOUND` 或 `FORBIDDEN` ,不得通过 `meeting_id` 、`note_file_id` 或 `file_id` 探测资源。
### 9.14.6 会议笔记-详情页-在线预览.png
<img src="images/会议笔记-详情页-在线预览.png" alt="会议笔记-详情页-在线预览" width="360">
**状态:基础文件读取已实现;会议笔记 ACL 和预览转换新增。 **
- 文件项返回 `preview.status=ready|processing|unsupported|failed` 、`preview.mode=native|converted|download_only` 和可选 `preview_file_id` ,客户端根据状态选择直接预览、轮询转换状态或提示暂不支持。
- 原生可预览文件使用受认证的 `GET /v0/file/s/{file_id}` ;转换预览使用 `GET /v0/file/s/{preview_file_id}` 。对象存储模式下服务端鉴权通过后返回 302 到短期签名 URL,客户端跟随 `Location` 获取文件内容。
- 服务端不得返回长期有效的公共下载 URL。客户端缓存键至少包含 `tenant_code + user_id + file_id + etag` ,退出登录或切换企业时清理受控缓存。
- `preview.status="processing"` 时客户端可在当前详情页低频刷新,建议 2 秒后开始、最长 30 秒;离开页面立即停止轮询。
### 9.14.7 会议笔记-详情页-文件下载中.png
<img src="images/会议笔记-详情页-文件下载中.png" alt="会议笔记-详情页-文件下载中" width="360">
**状态:复用统一下载任务;会议笔记 ACL 新增。 **
- 下载使用 `GET /v0/file/s/{file_id}` , Header 传 API Key 与 `X-MeetingIM-Auth: token <base64_token>` ,并支持 `Range` 和 `If-Range` 断点续传;对象存储模式下客户端跟随 302 `Location` 。
- 下载任务保存 `file_id,etag,total_bytes,downloaded_bytes,temp_path,state` 。Token 更新后使用新 Token 续传,禁止把 Token 写入任务文件或日志。
- 服务端在每次下载及续传时重新校验会议可见权限。返回 `FILE_FORBIDDEN` 时客户端停止重试;返回 `FILE_UNAVAILABLE` 时标记文件失效并刷新详情。
- 下载进度属于客户端本地状态,不更新会议笔记业务状态、不产生工作台通知,也不写入未读数。
### 9.14.8 会议笔记-文件预览.png
<img src="images/会议笔记-文件预览.png" alt="会议笔记-文件预览" width="360">
**状态:客户端预览页;服务端复用 9.14.6 文件读取协议。 **
- 预览页展示服务端返回的安全文件名和实际内容;标题不得直接信任未经清理的上传文件名。
- 图片、PDF、文本等受支持类型在应用内预览;Office 等类型优先使用受控转换文件,无法转换时按 `preview.mode="download_only"` 提示下载后打开。
- 文件被删除、审核拒绝、转换失败或权限撤销时,服务端返回稳定 `reason` ,客户端关闭预览并刷新详情,不能继续显示旧的授权链接。
- 从预览页返回详情后调用 `GET /v1/im/meetings/{meeting_id}/notes` 获取最新文件状态。
### 9.14.9 接口示例
以下示例用于会议笔记 HTTP 联调快速查阅。成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}` ,下表只展示 `data` 。
#### 9.14.9.1 工作台、会议记录员与会议笔记
| 接口 | 输入示例 | 成功 `data` 示例 |
| --- | --- | --- |
| `GET /v1/im/workbench` 需改造 | Query only | `{"layout_version":2,"modules":[{"code":"meeting_notifications","name":"会议通知","route":"/meetings","unread_count":3,"enabled":true,"permission":"enabled"},{"code":"meeting_notes","name":"会议笔记","route":"/meeting-notes","unread_count":0,"enabled":true,"permission":"enabled"}]}` |
| `POST /v1/im/meetings` 需改造 | Header `Idempotency-Key` ; Body `{"title":"项目周会","start_at":"2026-09-05T10:00:00+08:00","end_at":"2026-09-05T10:30:00+08:00","location":"会议室 A","recorder_user_id":"usr_me","attendee_user_ids":["usr_2"],"reminder_minutes":[10]}` | `{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","state":"scheduled","participants":[{"user_id":"usr_me","response":"accepted"},{"user_id":"usr_2","response":"pending"}],"permissions":{"can_update":true,"can_cancel":true},"version":1}` |
| `PATCH /v1/im/meetings/{meeting_id}` 需改造 | Header `Idempotency-Key` ; Body `{"recorder_user_id":"usr_2","version":1}` | `{"meeting_id":"mtg_1","recorder":{"user_id":"usr_2","display_name":"李四"},"version":2,"updated_at":"2026-09-05T01:30:00Z"}` |
| `GET /v1/im/meeting-notes?query=&cursor=&limit=20` 新增 | Query only | `{"items":[{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","location":"会议室 A","state":"ended","participant_count":2,"participant_names":["张三","李四"],"note_status":"empty","file_count":0,"permissions":{"can_view":true,"can_upload":true}}],"next_cursor":"","has_more":false}` |
| `GET /v1/im/meetings/{meeting_id}/notes` 新增 | Path meeting ID | `{"meeting":{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","location":"会议室 A","state":"ended","participants":[{"user_id":"usr_me","display_name":"张三"},{"user_id":"usr_2","display_name":"李四"}]},"note_status":"available","files":[{"note_file_id":"mnote_1","file_id":"file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf","uploader":{"user_id":"usr_me","display_name":"张三"},"created_at":"2026-09-05T03:00:00Z","version":1,"preview":{"status":"ready","mode":"native","preview_file_id":"file_1"},"permissions":{"can_preview":true,"can_download":true,"can_replace":true,"can_delete":true}}],"permissions":{"can_view":true,"can_upload":true},"version":1}` |
| `POST /v0/file/u/` | Multipart 文件;字段 `purpose=meeting_note` 、`meeting_id=mtg_1` ; Header `X-MeetingIM-Auth` | `{"ctrl":{"code":200,"params":{"file_id":"file_1","url":"/v0/file/s/file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf"}}}` |
| `POST /v1/im/meetings/{meeting_id}/notes/files` 新增 | Header `Idempotency-Key` ; Body `{"file_id":"file_1"}` | `{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf","created_at":"2026-09-05T03:00:00Z","version":1,"preview":{"status":"processing","mode":"converted"}}` |
| `PUT /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}` 新增 | Header `Idempotency-Key` ; Body `{"file_id":"file_2","version":1}` | `{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_2","name":"会议纪要-v2.pdf","version":2,"updated_at":"2026-09-05T03:10:00Z"}` |
| `DELETE /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}` 新增 | Header `Idempotency-Key` ; Query `version=2` | `{"note_file_id":"mnote_1","deleted":true,"deleted_at":"2026-09-05T03:20:00Z"}` |
| `GET /v0/file/s/{file_id}` | Header `X-MeetingIM-Auth` ;可带 `Range: bytes=0-` | 对象存储模式下鉴权通过后返回 302 到短期签名 URL;客户端跟随 Location 获取文件内容 |
### 9.14.10 字段与状态约定
| 字段 | 取值 | 前端处理 |
| --- | --- | --- |
| `meeting.state` | `scheduled` 、`in_progress` 、`ended` 、`canceled` | 会议笔记列表只展示 `ended` |
| `note_status` | `empty` 、`available` | `empty` 展示“暂无笔记”,`available` 展示文件列表 |
| `preview.status` | `ready` 、`processing` 、`unsupported` 、`failed` | 分别进入预览、短时刷新、仅下载提示、失败提示 |
| `preview.mode` | `native` 、`converted` 、`download_only` | 使用原文件、转换文件或仅提供下载 |
| `permissions.can_view` | Boolean | 是否可以查看当前会议笔记 |
| `permissions.can_upload` | Boolean | 是否显示并允许使用上传按钮,仅会议记录员为 `true` |
| `permissions.can_replace` | Boolean | 是否允许替换当前笔记文件 |
| `permissions.can_delete` | Boolean | 是否允许删除当前笔记文件 |
| `permissions.can_preview` | Boolean | 是否允许预览当前文件 |
| `permissions.can_download` | Boolean | 是否允许下载当前文件;首版普通参会人若产品要求“只能预览”,该值返回 `false` |
客户端必须以服务端返回的 `permissions` 为准。特别是普通参会人“只能预览”的要求,由文件项 `can_preview=true,can_download=false` 表达;即使预览底层使用文件读取接口,客户端也不得提供导出、系统分享或保存到相册入口。
### 9.14.11 错误码与客户端处理
| `reason` | 典型 HTTP 状态 | 场景 | 客户端处理 |
| --- | --- | --- | --- |
| `MEETING_NOT_ENDED` | 409 | 未结束会议上传或登记笔记 | 提示“会议结束后可上传笔记”,刷新会议详情 |
| `MEETING_CLOSED` | 409 | 会议结束后修改记录员或会议数据 | 切换为只读并刷新详情 |
| `NOT_MEETING_RECORDER` | 403 | 非记录员上传、替换或删除文件 | 隐藏操作入口并刷新权限 |
| `MEETING_NOTE_VERSION_CONFLICT` | 409 | 替换或删除时版本过期 | 拉取最新详情,不自动覆盖 |
| `MEETING_NOTE_FILE_INVALID` | 400 | 文件未上传完成、用途不符或不属于当前租户 | 提示重新上传 |
| `FILE_PREVIEW_PROCESSING` | 409 | 转换预览尚未完成 | 保持当前页并按退避策略刷新 |
| `FILE_PREVIEW_UNSUPPORTED` | 422 | 文件类型不支持在线预览 | 根据 `can_download` 决定是否提供下载 |
| `FILE_FORBIDDEN` | 403 | 当前用户无文件访问权限 | 关闭预览或停止下载并刷新详情 |
| `FILE_UNAVAILABLE` | 404/410 | 文件被删除、失效或审核拒绝 | 标记不可用并刷新文件列表 |
| `RESOURCE_NOT_FOUND` | 404 | 会议或笔记不可见、跨租户访问 | 返回列表,不提示资源是否真实存在 |
### 9.14.12 前后端联调与验收要点
1. 发起会议时不传 `recorder_user_id` ,响应中的记录员必须默认为当前发起人;指定其他记录员时,响应返回最终记录员和参会人名单。
2. 会议笔记列表只包含与当前用户有关的已结束会议;已结束但无文件的会议仍展示,详情为空状态。
3. 记录员进入详情显示上传按钮,普通参会人进入同一会议不显示上传按钮;直接调用写接口仍必须被服务端拒绝。
4. 普通参会人只允许在线预览时,响应为 `can_preview=true,can_download=false` ,客户端不展示下载、分享或导出入口。
5. 文件上传成功但会议笔记登记失败时,详情列表不出现该文件;重复登记同一幂等键返回首次结果。
6. 文件替换和删除必须校验 `version` ;旧版本请求返回冲突,不能覆盖新文件。
7. 进入列表、进入详情以及从上传/预览/下载页面返回时均主动刷新;断开 WebSocket 不影响会议笔记数据获取。
8. 会议笔记新增、替换、删除均不产生 Push、会议通知未读或工作台红点;工作台红点只随会议通知模块变化。
9. 跨租户、无关用户和已撤销权限用户无法通过会议 ID 或文件 ID 查看、预览、下载会议笔记。
10. 切换企业或退出登录后,会议笔记列表、详情、预览文件和下载临时数据按租户及账号隔离清理。
### 9.15 接口示例
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
@@ -2374,11 +2555,11 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}` 。
- 标注“待实现/需修复”的接口是产品目标协议,前端可按示例准备,但联调前以后端发布状态为准。
#### 9.14 .1 工作台、组织、打卡、会议、日报、月报、信箱
#### 9.15 .1 工作台、组织、打卡、会议、日报、月报、信箱
| 接口 | 输入示例 | 成功 `data` 示例 |
| --- | --- | --- |
| `GET /v1/im/workbench` | Query only | `{"layout_version":1 ,"modules":[{"code":"organization","name":"组织架构 ","icon_file_id":"","route":"/organization","unread_count":0,"enabled":true,"permission":"enabled"},{"code":"attendance ","name":"打卡考勤 ","icon_file_id":"","route":"/attendance" ,"unread_count":0,"enabled":true,"permission":"enabled"}],"enterprise":{"tenant_code":"default","feature_flags":{"organization":true,"attendance":true,"meetings":true}}}` |
| `GET /v1/im/workbench` | Query only | `{"layout_version":2 ,"modules":[{"code":"attendance","name":"打卡","icon_file_id":"","route":"/attendance","sort_order":10,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"monthly_reports","name":"月报","icon_file_id":"","route":"/reports/monthly","sort_order":20,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"daily_reports","name":"日报","icon_file_id":"","route":"/reports/daily","sort_order":30,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":" organization","name":"部门框架 ","icon_file_id":"","route":"/organization","sort_order":40," unread_count":0,"enabled":true,"permission":"enabled"},{"code":"executive_mailbox","name":"举报信箱","icon_file_id":"","route":"/executive-mailbox","sort_order":50,"unread_count":0,"enabled":true,"permission":"enabled"},{"code":"meetings ","name":"会议通知 ","icon_file_id":"","route":"/meetings","sort_order":60,"unread_count":3,"enabled":true,"permission":"enabled"},{"code":"meeting_notes","name":"会议笔记","icon_file_id":"","route":"/meeting-notes","sort_order":70 ,"unread_count":0,"enabled":true,"permission":"enabled"}],"enterprise":{"tenant_code":"default","feature_flags":{"organization":true,"attendance":true,"meetings":true,"meeting_notes":true }}}` |
| `GET /v1/im/organization/departments?parent_id=root&cursor=` | Query only | `{"items":[{"department_id":"dep_1","name":"研发部","parent_id":"root","child_count":0,"member_count":12,"version":1}],"next_cursor":""}` |
| `GET /v1/im/organization/departments/{id}/members?cursor=` | Query only | `{"items":[{"user_id":"usr...","display_name":"张三","avatar_file_id":"","department_id":"dep_1","title":"工程师","can_contact":true}],"next_cursor":""}` |
| `GET /v1/im/organization/members?query=&department_id=&cursor=` 待实现/可由部门成员接口替代 | Query only | `{"items":[{"user_id":"usr...","display_name":"张三","department_id":"dep_1","department_name":"研发部","can_contact":true}],"next_cursor":""}` |
@@ -2390,7 +2571,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| `POST /v1/im/attendance/appeals` | Header `Idempotency-Key` ; Body `{"record_id":"evt_...","appeal_type":"missing_checkin","reason":"外出拜访客户,忘记打卡","target_action":"check_in","target_time":"2026-08-31T09:00:00+08:00","attachment_file_ids":["file_1"]}` | `{"appeal_id":"atta_...","record_id":"evt_...","user_id":"usr...","appeal_type":"missing_checkin","reason":"外出拜访客户,忘记打卡","target_action":"check_in","target_time":"2026-08-31T01:00:00Z","attachment_file_ids":["file_1"],"status":"pending","submitted_at":"2026-08-31T03:00:00Z","version":1}` |
| `GET /v1/im/attendance/appeals?scope=mine&status=pending&cursor=` | Query only | `{"items":[{"appeal_id":"atta_...","record_id":"evt_...","user_id":"usr...","appeal_type":"missing_checkin","status":"pending","submitted_at":"2026-08-31T03:00:00Z","version":1}],"next_cursor":"","has_more":false}` |
| `GET /v1/im/meetings?cursor=` 待补列表 | Query only | `{"items":[{"meeting_id":"mtg_1","title":"周会","start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","my_response":"pending","state":"scheduled"}],"next_cursor":""}` |
| `POST /v1/im/meetings` | `{"title":"周会","start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","location":"线上","attendee_user_ids":["usr..."],"reminder_minutes":[10]}` | `{"meeting_id":"mtg_1","title":"周会","start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","location":"线上","participants":[{"user_id":"usr_me","response":"accepted"},{"user_id":"usr...","response":"pending"}],"permissions":{"can_update":true,"can_cancel":true,"can_remind":true,"can_respond":false},"version":1}` |
| `POST /v1/im/meetings` | `{"title":"周会","start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","location":"线上","recorder_user_id":"usr_me"," attendee_user_ids":["usr..."],"reminder_minutes":[10]}` | `{"meeting_id":"mtg_1","title":"周会","recorder":{"user_id":"usr_me","display_name":"张三"}," start_at":"2026-08-21T10:00:00Z","end_at":"2026-08-21T10:30:00Z","location":"线上","participants":[{"user_id":"usr_me","response":"accepted"},{"user_id":"usr...","response":"pending"}],"permissions":{"can_update":true,"can_cancel":true,"can_remind":true,"can_respond":false},"version":1}` |
| `GET /v1/im/meetings/{meeting_id}` | Path meeting ID | `{"meeting_id":"mtg_1","title":"周会","participants":[{"user_id":"usr...","response":"pending"}],"permissions":{"can_respond":true},"version":1}` |
| `PATCH /v1/im/meetings/{id}` | `{"title":"新标题","version":1}` | `{"meeting_id":"mtg_1","title":"新标题","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/meetings/{id}/cancel` | `{"version":2,"reason":"时间冲突"}` | `{"meeting_id":"mtg_1","state":"cancelled","version":3}` |
@@ -2655,7 +2836,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| `GET /v1/im/security/realname` | Query only | `{"status":"unverified","can_submit":true}` |
| `POST /v1/im/security/realname/submissions` | `{"name":"张三","id_type":"cn_id","id_number":"110101199001011234"}` | `{"submission_id":"rn_1","status":"pending","can_submit":false}` |
| `POST /v1/im/security/realname/appeals` 待实现 | `{"reason":"证件变更","description":"需要更正实名信息","action_token":"action_token"}` | `{"appeal_id":"rna_1","status":"pending","submitted_at":"2026-08-20T09:00:00Z"}` |
| `PUT /v1/im/devices/{device_id}/push-token` | `{"provider":"fcm ","token":"push_token ","platform":"android","app_version":"1.0.0"}` | `{"device_id":"device_1","provider":"fcm ","platform":"android","app_version":"1.0.0","updated_at":"2026-08-20T09:00:00Z"}` |
| `PUT /v1/im/devices/{device_id}/push-token` | `{"provider":"jpush ","token":"registration_id ","platform":"android","app_version":"1.0.0","push_env":"test","sdk_version":"5.0.0","notification_permission":"granted" }` | `{"device_id":"device_1","provider":"jpush ","platform":"android","app_version":"1.0.0","updated_at":"2026-08-20T09:00:00Z"}` |
| `DELETE /v1/im/security/sessions/{session_id}` 待实现 | Header `X-Action-Token: action_token` | `{"session_id":"sess_1","revoked":true}` |
## 11. 反诈安全、举报与意见反馈
@@ -2749,12 +2930,12 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| P0 | 企业码解析、验证码、密码重置、二次认证、Token `auth_version` 、登录换新和撤销 | 1.1~ 1.11、10.7~ 10.8 |
| P0 | 普通用户禁止发布系统 Topic;系统通知和业务事件可信生产通道 | 2.5、3.1~ 3.5、6.4、11.1 |
| P0 | 消息 `x-im-schema/type/client-mid` 校验和服务端幂等 | 2~4 章 |
| P0 | 文件资源级 ACL、Range、ETag、用途绑定和租户存储配额 | 3.6、4.10~ 4.14、6.27~ 7.16、11.4~ 11.5 |
| P0 | 文件资源级 ACL、Range、ETag、用途绑定和租户存储配额 | 3.6、4.10~ 4.14、6.27~ 7.16、9.14、 11.4~ 11.5 |
| P1 | 好友申请、好友关系、黑名单状态机 | 第 3、5 章 |
| P1 | 群成员、角色、禁言、公告、邀请审批和配额 | 第 6 章 |
| P1 | 收藏、转发和聊天记录搜索 | 第 4、7 章 |
| P1 | 通话媒体状态、结果消息、Push 来电 | 第 8 章 |
| P2 | 工作台、组织、会议、日报、月报、信箱 | 第 9 章 |
| P2 | 工作台、组织、会议、会议笔记、 日报、月报、信箱 | 第 9 章 |
| P2 | 隐私、青少年模式、反诈、举报、反馈 | 第 10、11 章 |
### 12.3 前后端联调最低验收用例