Files
EchoChat/docs/api/websocket.md
bujinyuan f97fec24a8 feat(phase2e-2): 前端 mediasoup-client 集成 + Pinia meeting Store(Task 9)
- frontend/src/api/meeting.js:12 个 REST 接口封装,统一 unwrap envelope.data
- frontend/src/services/websocket.js:新增 sendWithAck(Promise 化 + 超时 + 序列号)
- frontend/src/utils/mediasoup-client.js:MediaEngine 包装 Device/Transport/Producer/Consumer
- frontend/src/store/meeting.js:Pinia 会议状态机,桥接 14 个 WS 事件 + cleanupStaleMeetings
- frontend/src/constants/meeting.js:状态枚举 + 事件名集中管理
- frontend/src/pages/meeting/debug.vue:临时调试页(H5 原生 video/audio DOM 绕过 uni 组件限制)
- backend:meeting.consume.resume WS 事件 + create/join 响应透传 router_id + rtp_capabilities
- 文档:frontend/meeting.md、websocket.md、CURRENT_STATUS、plan 全部同步 Task 9 落地

Made-with: Cursor
2026-04-22 11:22:49 +08:00

519 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WebSocket 事件协议
> 通用规范(认证方式、响应格式、错误码)见 [README.md](README.md)
> 本文档定义 EchoChat 系统中所有 WebSocket 实时通信事件。
---
## 连接说明
### 连接地址
| 环境 | 地址 |
|------|------|
| 开发环境 | `ws://localhost:8080/ws?token=<access_token>` |
| 生产环境 | `wss://api.echochat.com/ws?token=<access_token>` |
### 连接认证
通过 URL 查询参数 `token` 携带 JWT Access Token服务端验证通过后建立连接。
### 心跳机制
- 客户端每 **30 秒** 发送一次 ping 帧
- 服务端响应 pong 帧
- 如果 **90 秒** 内未收到客户端心跳,服务端主动断开连接
### 断线重连
- 客户端检测到连接断开后自动重连
- 重连间隔采用指数退避1s → 2s → 4s → 8s → 16s → 最大 30s
- 重连成功后拉取离线消息
---
## 消息格式
### 客户端发送格式
```json
{
"event": "im.message.send",
"seq": 1001,
"data": { ... },
"time": "2026-02-27 18:06:40"
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| event | string | 事件名称,格式:`{模块}.{对象}.{动作}` |
| seq | int | 消息序列号,客户端自增,用于匹配请求和响应 |
| data | object | 事件数据 |
| time | string | 发送时间,格式:`yyyy-MM-dd HH:mm:ss`,时区 Asia/Shanghai |
### 服务端响应格式ACK
```json
{
"event": "im.message.send.ack",
"seq": 1001,
"code": 0,
"message": "ok",
"data": { "msg_id": 10086 }
}
```
### 服务端推送格式
```json
{
"event": "im.message.new",
"data": { ... },
"time": "2026-02-27 18:06:40"
}
```
推送类消息没有 seq 字段(不需要客户端确认)。
---
## 即时通讯事件
### im.message.send
**方向:** 客户端 → 服务端
**说明:** 发送消息到会话。`conversation_id``target_user_id` 二选一:首次发消息使用 `target_user_id`(自动创建会话),后续使用 `conversation_id`
**data 参数:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| conversation_id | int | 否 | 已有会话 ID与 target_user_id 二选一) |
| target_user_id | int | 否 | 对方用户 ID首次发消息时使用 |
| type | int | 是 | 消息类型1=文本 |
| content | string | 是 | 文本内容 |
| client_msg_id | string | 否 | 客户端消息唯一 ID用于幂等去重 |
**ACK 响应 data**
```json
{
"id": 10086,
"conversation_id": 1,
"sender_id": 1,
"type": 1,
"content": "你好",
"status": 1,
"client_msg_id": "xxxx-xxxx",
"created_at": "2026-03-03 10:30:00"
}
```
---
### im.message.new
**方向:** 服务端 → 客户端(推送)
**说明:** 收到新消息推送
**data 内容:**
```json
{
"id": 10086,
"conversation_id": 1,
"sender_id": 2,
"sender_name": "李四",
"sender_avatar": "https://...",
"type": 1,
"content": "你好",
"client_msg_id": "",
"created_at": "2026-03-03 10:30:00"
}
```
---
### im.message.recall
**方向:** 客户端 → 服务端
**说明:** 撤回消息(发送后 2 分钟内)。撤回成功后若该消息是会话最后一条,会同步更新会话预览为"XX 撤回了一条消息"。
**data 参数:** `{ "message_id": 10086 }`
---
### im.message.recalled
**方向:** 服务端 → 客户端(推送)
**说明:** 消息被撤回通知
**data 内容:** `{ "message_id": 10086, "conversation_id": 1, "sender_id": 2 }`
---
### im.conversation.read
**方向:** 客户端 → 服务端
**说明:** 标记会话已读(清零未读数 + 更新 Redis 全局未读数)
**data 参数:** `{ "conversation_id": 1 }`
---
### im.typing
**方向:** 双向(客户端发送 → 服务端转发给对方)
**说明:** 正在输入通知。客户端发送后服务端转发给对方,前端收到后设置 3 秒超时自动清除。
**data 参数(客户端发送):** `{ "conversation_id": 1 }`
**data 内容(服务端推送):** `{ "conversation_id": 1, "user_id": 2 }`
---
### im.offline.sync
**方向:** 服务端 → 客户端(推送)
**说明:** WebSocket 连接成功后服务端主动推送离线未读摘要
**data 内容:**
```json
{
"total_unread": 5,
"conversations": [
{
"conversation_id": 1,
"unread_count": 3,
"last_msg_content": "你好",
"last_msg_time": "2026-03-03 10:30:00"
}
]
}
```
---
## 会议信令事件Phase 2e-2 Task 6 已落地Task 7 起 mediasoup 返回值为真实值)
> **SSOT**:会议相关的全部 WS 事件详细契约(请求/ACK/广播载荷、权限、错误处理、资源追踪)见 [`docs/api/frontend/meeting.md`](./frontend/meeting.md) §WebSocket 信令协议。本节仅列事件总览。
| # | 方向 | 事件 | 用途 |
|---|------|------|------|
| 1 | C→S | `meeting.room.join` | 声明加入某会议 WS 频道(需已在 REST 层完成 `/join`|
| 2 | C→S | `meeting.room.leave` | 离开 WS 频道并清理媒体资源(不改 participant 表)|
| 3 | C→S | `meeting.member.state.changed` | 更新自己的 audio/video/hand_raisedhost 可指定 `target_user_id` 强制静音他人 |
| 4 | C→S | `meeting.transport.create` | 创建 mediasoup WebRtcTransport`direction: send/recv`|
| 5 | C→S | `meeting.transport.connect` | 提交 DTLS Parameters |
| 6 | C→S | `meeting.produce.start` | 创建 Producer |
| 7 | C→S | `meeting.consume.start` | 创建 Consumer |
| 8 | C→S | `meeting.producer.close` | 关闭自己的 Producer |
| 9 | C→S | `meeting.consume.resume` | 客户端完成 track 挂载后请求 resume ConsumerTask 9 新增,不广播)|
| 10 | S→C | `meeting.member.joined` | 新成员加入广播REST /join 触发)|
| 11 | S→C | `meeting.member.left` | 成员离开广播REST /leave /kick 或 WS 资源清理)|
| 12 | S→C | `meeting.member.kicked` | 定向通知被踢者 |
| 13 | S→C | `meeting.host.changed` | 主持人变更 |
| 14 | S→C | `meeting.room.ended` | 会议被结束 |
| 15 | S→C | `meeting.member.state.changed` | 成员状态变化广播 |
| 16 | S→C | `meeting.member.producer.new` | 成员开启/关闭媒体流(`closed=true` 表示关闭)|
| 17 | S→C | `meeting.chat` | 会议内聊天REST /chats 触发)|
### 协议约定
- **C→S 白名单**:客户端仅可发起上表 C→S 列的 9 个事件Task 9 扩到 9其余 `meeting.*` 事件若由客户端发送被静默丢弃。
- **ACK 规则**:每个 C→S 事件服务端必回 `<event>.ack`;成功 `code=0`,业务失败 `code=-1` + 中文 `message`(与 REST 领域错误口径一致)。
- **错误码示例**`会议不存在` / `你当前未在会议中` / `仅主持人可执行此操作` / `会议已结束`
- **资源追踪**:服务端对每用户在每会议的 transport/producer/consumer 用 Redis Set `echo:meeting:resources:{room_id}:{user_id}` 记录WS 断开或 `room.leave` 时自动清理。
- **真实 mediasoupTask 7 起)**`meeting.transport.create` ACK 的 `id` / `iceParameters` / `iceCandidates` / `dtlsParameters`,以及 `meeting.produce.start` / `meeting.consume.start` 的 ID 均来自 Node media-server 真实 mediasoup WorkerNode 404如关闭不存在 producer在 Go 侧幂等转 `code=0`
---
## 用户状态事件
### user.status.online
**方向:** 服务端 → 客户端
**说明:** 好友上线通知
**data 内容:** `{ "user_id": 2, "nickname": "李四" }`
---
### user.status.offline
**方向:** 服务端 → 客户端
**说明:** 好友离线通知
**data 内容:** `{ "user_id": 2 }`
---
## 通知事件
### notify.new
**方向:** 服务端 → 客户端
**说明:** 新通知推送(通用)
**data 内容:** 与 Notify API 获取通知列表中的单条通知格式一致
---
### notify.meeting.invite
**方向:** 服务端 → 客户端
**说明:** 会议邀请推送
**data 内容:**
```json
{
"room_code": "123-456-789",
"title": "产品需求讨论",
"from_user_id": 1,
"from_nickname": "张三"
}
```
---
### notify.friend.request
**方向:** 服务端 → 客户端
**说明:** 好友申请推送
**data 内容:**
```json
{
"friendship_id": 5,
"from_user_id": 2,
"from_nickname": "李四",
"from_avatar": "https://...",
"message": "我是你的同事"
}
```
---
## 群聊事件Phase 2c
### im.group.read
**方向:** 客户端 → 服务端
**说明:** 标记群聊消息已读(消息级已读回执)
**data 内容:**
```json
{
"conversation_id": 100,
"message_ids": [501, 502, 503]
}
```
**ACK 响应:** `{ "code": 0, "message": "ok" }`
---
### group.created
**方向:** 服务端 → 客户端
**说明:** 群聊创建成功,推送给所有初始成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"name": "项目讨论组",
"owner_id": 1
}
```
---
### group.info.update
**方向:** 服务端 → 客户端
**说明:** 群信息更新(名称、头像、公告等),推送给所有群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"operator_id": 1
}
```
---
### group.dissolved
**方向:** 服务端 → 客户端
**说明:** 群聊已解散,推送给所有群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"operator_id": 1
}
```
---
### group.member.join
**方向:** 服务端 → 客户端
**说明:** 新成员加入群聊(邀请加入或审批通过),推送给所有群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"user_ids": [5, 6]
}
```
---
### group.member.kicked
**方向:** 服务端 → 客户端
**说明:** 成员被移出群聊,推送给所有群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"user_id": 5,
"operator_id": 1
}
```
---
### group.member.leave
**方向:** 服务端 → 客户端
**说明:** 成员主动退出群聊,推送给其余群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"user_id": 5
}
```
---
### group.role.update
**方向:** 服务端 → 客户端
**说明:** 成员角色变更(设为/取消管理员),推送给所有群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"user_id": 5,
"new_role": 1,
"operator_id": 1
}
```
> `new_role`0=普通成员1=管理员2=群主
---
### group.mute.update
**方向:** 服务端 → 客户端
**说明:** 禁言状态变更(个人禁言或全体禁言),推送给所有群成员
**data 内容(个人禁言):**
```json
{
"group_id": 10,
"conversation_id": 100,
"user_id": 5,
"is_muted": true,
"operator_id": 1
}
```
**data 内容(全体禁言):**
```json
{
"group_id": 10,
"conversation_id": 100,
"is_all_muted": true,
"operator_id": 1
}
```
---
### group.owner.transfer
**方向:** 服务端 → 客户端
**说明:** 群主转让,推送给所有群成员
**data 内容:**
```json
{
"group_id": 10,
"conversation_id": 100,
"old_owner_id": 1,
"new_owner_id": 5
}
```
---
### group.join.request
**方向:** 服务端 → 客户端
**说明:** 新的入群申请,推送给群主和管理员
**data 内容:**
```json
{
"group_id": 10,
"request_id": 20,
"user_id": 8,
"user_nickname": "新用户",
"message": "请让我加入"
}
```