本次提交一次性落盘 Phase 2e-2 的 Task 0(PoC Spike)、Task 1(骨架 + Fastify 5 升级) 和 Task 2(9 个内部 REST API + code-reviewer 审查修复),覆盖 media-server 子项目 从零到可用的全部工作。 【Task 0 - PoC Spike】 - media-server/poc/:Node + mediasoup + fastify-websocket + 前端 mediasoup-client - Playwright 双 tab 自动化验证 2 人会议:4 transports / 4 producers / 4 consumers / RSS 61MB - media-server/docs/poc-notes.md 归档 7 项关键坑 + 启动步骤 + Task 1/2/9 复用映射 - 锁定技术栈:mediasoup + fastify + mediasoup-client,不改用 livekit-server 【Task 1 - 骨架 + Fastify 5 升级】 - 依赖版本:mediasoup@3.19.0 + fastify@5.8.5 + fastify-plugin@5.1.0 + @fastify/sensible@6.0.4 + @fastify/websocket@11.2.0 + pino@9.3.2 + zod@3.23.8 - src 五件套:app.ts / config.ts / utils/logger.ts / mediasoup/worker.ts / middlewares/internal-auth.ts - /healthz + /readyz + /internal/info 三端点实测通过 - X-Internal-Token 鉴权用 timingSafeEqual 防侧信道 - mediasoup Worker died 指数退避自愈通过 kill -9 验证 - 多阶段 Dockerfile:非 root + curl HEALTHCHECK + 暴露 40000-40199 UDP/TCP - Fastify 4 → 5 升级:loggerInstance: logger 消灭两个 pino 实例 【Task 2 - 9 个内部 REST API + 审查修复】 接口全部挂 /internal/v1/* 前缀,覆盖 Router/Transport/Producer/Consumer 完整生命周期: - POST /routers, DELETE /routers/:id - POST /transports, POST /transports/:id/connect - POST /producers, DELETE /producers/:id - POST /consumers, POST /consumers/:id/resume, DELETE /consumers/:id 工程特性: - zod 手动 parse + 全局 errorHandler(不引入 fastify-type-provider-zod 避免 zod v4 依赖冲突) - AppError 统一错误码(NOT_FOUND/CONFLICT/CAN_NOT_CONSUME/ROUTER_LIMIT_EXCEEDED /MEDIASOUP_ERROR/VALIDATION_ERROR/UNAUTHORIZED/INTERNAL_ERROR) - 所有资源用 Map + observer.once('close') 自清理,Consumer 监听 producerclose 级联关闭 - Consumer 强制 paused:true 创建,/resume 独立接口 - Transport direction 强约束:recv 拒 produce、send 拒 consume code-reviewer 子代理审查"有条件通过",同步修复: - M1 _clearXxxMap 新增 src/utils/test-guard.ts#assertTestOnly 守卫(生产误调用抛错) - M2 新增 src/schemas/rtp.ts 对 rtpParameters / rtpCapabilities 做 codecs 浅层校验 (mimeType/clockRate/payloadType 必填、codecs ≥1),消除 as unknown as 双跳断言 - m1 connectTransport 改乐观锁:先置位再 await,失败回退 - m2 改读 consumer.producerPaused(更符合 mediasoup 语义) - m3 producerclose 改为 once(风格一致) - m5 internal-auth 改为反向白名单 PRIVATE_PATH_PREFIXES = ['/internal/'] 验证: - typecheck / lint 0 错误 - vitest:65 passed / 8 spec 文件 - 覆盖率 stmts 82.87% / branches 75.83% / funcs 91.3% / lines 82.87% - 9 接口 happy path + 6 类错误路径 curl 手测全部按预期返回 【文档同步】 - CURRENT_STATUS.md +232 行:新增 Task 0/1/2 完整记录 + 代码审查修复章节 + 延后清单 - project-context.mdc:Task 0/1/2 状态同步 - phase2e-2-design.md +20 行:Fastify 5 升级相关决策记录 - phase2e-2-implementation.plan.md +117 行:Task 0/1/2 实际产出 + 修复记录 余下 Minor/Nits(m4/m6~m10/n1~n10)登记至 Task 16 收尾清单一次性清扫。 Made-with: Cursor
33 KiB
33 KiB
Phase 2e-2 实施计划:会议 MVP(多人音视频)
状态: 📋 待执行(设计文档定稿后进入代码开发) 设计文档: Phase 2e-2 设计文档 上级路线图: Phase 2e 整体路线图 分支:
feature/phase2e-2-meeting-mvp预估总工时: 约 17 人日(17 个 Task,含 PoC 与 UI 打磨) 最后更新: 2026-04-21(实施计划首版落盘)
一、范围锁定
本期交付(与设计文档 §2.2.1 一一对应)
- 即时会议创建 / 加入 / 离开 / 结束 / 列表 / 详情
- 会议号
XXX-XXX-XXX+ 可选密码(bcrypt)+ 邀请链接 + 通知中心邀请(meeting_invite) - 入会前设备预览页(设备选择 + 本地预览 + 音量检测)
- ≤ 8 人音视频(mediasoup SFU + simulcast 3 档)
- 主持人四件套:静音他人 / 移除成员 / 转让主持人 / 结束会议
- 会议生命周期:host 掉线 2 分钟宽限 + host 转让(最早加入者)+ 空房 5 分钟 TTL
- 会议内文字聊天(独立
meeting_chats表,24 小时后清理) - 响应式布局:桌面 3×3 / 平板 2×2 / 手机 单列+抽屉
- 双态部署:本机 Docker Compose + 公网
announcedIp+ coturn--profile public
显式推迟(与设计文档 §2.2.2 一一对应)
预约会议 / 提醒 / 等候室 / 锁定 / 屏幕共享 / 录制 / 虚拟背景 / 联合主持人 / 管理端会议管理 / 多 Worker 集群。
二、Task 依赖拓扑
flowchart LR
T0[Task 0 mediasoup PoC Spike] --> T1[Task 1 media-server 骨架]
T1 --> T2[Task 2 Node 9 个 REST API]
T0 --> T3[Task 3 数据库 DDL]
T3 --> T4[Task 4 Go meeting 模块骨架]
T4 --> T5[Task 5 会议 REST 接口]
T4 --> T6[Task 6 WS 信令处理器]
T2 --> T7[Task 7 Go-Node HTTP Client]
T6 --> T7
T5 --> T8[Task 8 生命周期状态机]
T2 --> T9[Task 9 前端 mediasoup-client + Store]
T5 --> T10[Task 10 前端预览/创建/加入页]
T9 --> T11[Task 11 会议室主页]
T11 --> T12[Task 12 会议内聊天面板]
T5 --> T13[Task 13 meeting_invite 通知对接]
T11 --> T14[Task 14 docker-compose + 双态配置]
T11 --> T15[Task 15 UI/UX 打磨]
T15 --> T16[Task 16 E2E + 代码审查 + 文档同步]
T12 --> T16
T13 --> T16
T14 --> T16
T8 --> T16
关键路径:T0 → T1 → T2 → T9 → T11 → T15 → T16(≈ 9 人日) 并行机会:T3/T4 可与 T1/T2 并行;T10/T13 可与 T11 并行;T14 可与 T15 并行。
三、文件与变更清单概览
3.1 新建子项目
media-server/—— 根级子项目(与backend//frontend//admin/并列)src/app.ts/src/config.tssrc/mediasoup/{worker.ts, router.ts, codecs.ts}src/routes/{router,transport,producer,consumer}.route.tssrc/services/{router,transport,producer,consumer}.service.tssrc/middlewares/internal-auth.tssrc/utils/logger.tstests/*.spec.tspackage.json/tsconfig.json/Dockerfile/.env.example
3.2 后端新增(backend/go-service/app/meeting/)
| 文件 | 作用 |
|---|---|
constants/{meeting_status,meeting_role,ws_events}.go |
常量 |
model/{meeting_room,meeting_participant,meeting_chat}.go |
GORM 模型 |
dao/{meeting_room_dao,meeting_participant_dao,meeting_chat_dao}.go |
DAO |
service/meeting_service.go |
房间 CRUD + 生命周期 |
service/meeting_signal_service.go |
WS 信令处理 |
service/meeting_chat_service.go |
会议内聊天 |
service/node_client.go |
Go → Node HTTP 客户端 |
service/interfaces.go |
对外注入接口(NotifyPusher / UserInfoResolver) |
controller/{meeting_controller,meeting_chat_controller}.go |
REST Controller |
controller/meeting_ws_handler.go |
WS 事件处理(注册到 ws.Hub) |
router/meeting_router.go |
路由注册 |
task/meeting_cleanup_task.go |
定时任务:空房 TTL 兜底 + chat 24h 清理 |
provider/{provider,wire_gen}.go |
Wire |
3.3 后端改造
| 文件 | 改动 |
|---|---|
app/provider/wire.go |
注册 MeetingSet;绑定 meetingService.NotifyPusher = notifyService、meetingService.UserInfoResolver = userService、ws.Hub.MeetingSignalDispatcher = meetingSignalService |
app/provider/provider.go |
App 新增 MeetingController / MeetingChatController / MeetingCleanupTask |
cmd/server/main.go |
启动 app.MeetingCleanupTask.Start() + defer Stop() |
router/router.go |
注册 meeting 路由 |
app/ws/hub.go / app/ws/handler.go |
新增 MeetingSignalDispatcher 接口注入;WS 断线时触发 meeting.OnUserDisconnect |
deploy/docker/postgres/init.sql |
追加 meeting_rooms / meeting_participants / meeting_chats DDL + 索引 |
app/dto/meeting_dto.go |
请求 / 响应 DTO |
3.4 前端新增(frontend/src/)
| 文件 | 作用 |
|---|---|
api/meeting.js |
REST API 封装 |
constants/meeting.js |
会议类型 / 角色 / WS 事件常量 |
store/meeting.js |
Pinia Store |
utils/mediasoup-client.js |
Device/Transport/Producer/Consumer 封装 |
pages/meeting/{index,create,join,preview,room}.vue |
5 个页面 |
components/meeting/{VideoTile,VideoGrid,MeetingToolbar,MemberPanel,ChatPanel,InviteDialog,DevicePreview,NetworkBadge}.vue |
8 个组件 |
3.5 前端改造
| 文件 | 改动 |
|---|---|
pages.json |
新增 5 条 page 注册 |
utils/ws.js |
增加 meeting 事件分发 |
components/NotifyItem.vue |
meeting_invite 卡片补齐「立即加入 / 稍后」 |
store/notify.js |
meeting_invite 点击跳转 /pages/meeting/preview?code=xxx |
3.6 Docker / 脚本
| 文件 | 改动 |
|---|---|
deploy/docker-compose.dev.yml |
新增 media-server 服务;新增 coturn 服务(profiles: [public]) |
scripts/start.sh |
增加 media 子命令(启动 media-server)与 full 子命令(全部服务) |
scripts/stop.sh |
对应 stop |
scripts/status.sh |
增加 media-server 进程/容器检测 |
scripts/deploy-public.sh(新建) |
公网部署快捷脚本(校验 announcedIp、端口开放) |
.env.example / .env.local.example |
新增 MEDIASOUP_* / MEDIA_INTERNAL_TOKEN / TURN_* |
四、Task 明细
Task 0:mediasoup PoC Spike ✅
- 状态:✅ 已完成(2026-04-21)
- 目标:在正式动工前用最小代码跑通"2 个浏览器 ↔ Node ↔ mediasoup ↔ 互相看见视频",验证技术栈可用性与关键参数
- 依赖:无(起点)
- 实际产出:
media-server/poc/:Fastify + WS 信令 + mediasoup Worker/Router 约 500 行代码server.mjs(248 行)+public/client.mjs(215 行)+public/index.html- 依赖固定版本:
mediasoup@3.14.11/fastify@4.28.1/@fastify/websocket@10.0.1/pino@9.3.2,Node 24 下 C++ 编译 53 秒完成 - PoC 结论文档:
media-server/docs/poc-notes.md(9 章节:架构图 / 实测数据 / 7 项关键坑 / 技术栈判定 / 复用映射 / 启动步骤)
- 实测数据(Playwright 双 tab 自动化验证):
- 2 人会议:4 transports / 4 producers / 4 consumers / RSS 61MB
- 线性外推 8 人会议:16 transports / 16 producers / 112 consumers / 约 200MB RSS(Node 单进程毫无压力)
- peer 关闭后资源自动清理(consumers 从 4 → 0),无泄漏
- 已锁定的关键坑:
- 本机 Demo
MEDIASOUP_ANNOUNCED_IP必须留空(非127.0.0.1),让 Chromium 自动替换0.0.0.0为可用地址 - mediasoup-client 无 UMD bundle,PoC 走
esm.shCDN;Task 9 正式前端改用npm + vite - Consumer 必须以
paused:true创建,客户端transport.consume后再调resumeConsumer,否则首帧丢失 - Worker
died事件必须监听 + 外部进程管理器重启
- 本机 Demo
- 决策结论:维持 mediasoup + fastify + mediasoup-client 选型,不改用 livekit-server。技术栈可用性已满足 MVP 需求。
- 工作量:1 人日(实际用时吻合估算)
Task 1:media-server 项目骨架
- 状态:✅ 已完成(2026-04-21)
- 目标:创建正式
media-server/目录,包含 TS 配置、Fastify 入口、日志、中间件、Dockerfile - 依赖:T0
- 实际产出:
media-server/package.json— 锁定(已在同日升级至 v5 生态)mediasoup@3.19.0 / fastify@5.8.5 / fastify-plugin@5.1.0 / @fastify/sensible@6.0.4 / @fastify/websocket@11.2.0 / pino@9.3.2 / zod@3.23.8;开发链:tsx / typescript@5.5 / eslint / prettier / vitestmedia-server/tsconfig.json+tsconfig.build.json— TS 5.5 严格模式、ES2022 + Bundler 解析media-server/.eslintrc.json+.prettierrc— @typescript-eslint + prettier 协同,强制consistent-type-importsmedia-server/.env.example+.gitignore+.dockerignore— 双态部署环境变量模板media-server/src/config.ts(110 行)— dotenv 加载 + zod 严格校验 + 端口区间交叉校验 + 校验失败process.exit(1)media-server/src/utils/logger.ts(45 行)— pino + pino-pretty(dev 可选)+ token redact +childLoggermedia-server/src/mediasoup/worker.ts(115 行)— Worker 单例 +died事件指数退避重启(1s → 30s 封顶)media-server/src/middlewares/internal-auth.ts(55 行)—fastify-plugin包装 onRequest hook +timingSafeEqual防侧信道 +/healthz//readyz白名单media-server/src/app.ts(125 行)— Fastify 入口 +/healthz+/readyz(未就绪 503)+/internal/info+ 优雅停机(SIGINT/SIGTERM/unhandledRejection/uncaughtException)media-server/Dockerfile— 多阶段(node:20-bookworm-slim),builder 装 python3 + build-essential 编译 mediasoup worker,runtime 裁 dev deps + 非 root 用户 + curl HEALTHCHECK + 显式暴露40000-40199/UDP+TCPmedia-server/README.md— 目录结构 / 快速开始 / 鉴权校验 / Docker 构建 / 配置说明 / Task 1 验收清单
- 实测验收(已通过):
npm install成功(315 包,mediasoup C++ worker 编译通过)npm run typecheck0 错误、npm run lint0 错误npm run dev→ 2s 内 "media-server listening"curl /healthz→{"ok":true,"mediasoupVersion":"3.19.0","workerPid":<pid>,"workerRestartAttempts":0}curl /readyz→{"ready":true}- 未带/错误
X-Internal-Token→ HTTP 401;正确 token → 200 返回 mediasoup 版本 / worker 状态 kill -9 <workerPid>→ 1s 后自动拉起新 Worker(PID 变更),日志出现 "worker died" → "worker started"
- 关键决策:
- Fastify 5 + 单 pino 实例:首轮骨架先用 v4 跑通,发现 Fastify 4 已过 LTS(2025-06-30)且无
loggerInstance导致两个 pino 实例;同日升级至 fastify@5.8.5,回归单实例 + 官方维护版本,插件(@fastify/websocket 11 / @fastify/sensible 6 / fastify-plugin 5)已 GA 支持 v5 - mediasoup 3.19 类型从
mediasoup/types子路径引入(Worker/WorkerLogLevel) - 内部鉴权使用
timingSafeEqual替代===,对长度不等先拉齐再比,防 token 长度时序探测 - Worker 指数退避
1s → 2s → 4s → 8s → 16s → 30s(封顶),重启成功后restartAttempts归零 - Fastify 5 破坏性变更逐条核查通过:Node ≥20(Dockerfile 已满足)/ schema 完整 JSON Schema(Task 2 配合 zod)/
.listen()对象签名 / plugin 纯 async
- Fastify 5 + 单 pino 实例:首轮骨架先用 v4 跑通,发现 Fastify 4 已过 LTS(2025-06-30)且无
- 工作量:0.5 人日(实际用时吻合估算,含 Fastify 4→5 升级 30 分钟)
Task 2:Node 侧 9 个 REST API ✅(2026-04-21 完成)
- 目标:实现设计文档 §7.2 的 9 个接口,mediasoup 资源完整生命周期
- 依赖:T1
- 实际产出:
src/schemas/{common,router,transport,producer,consumer}.schema.ts(5 文件,zod schema 全覆盖 body/params/DTLS fingerprints)src/utils/errors.ts(AppError+NOT_FOUND/CONFLICT/CAN_NOT_CONSUME/ROUTER_LIMIT_EXCEEDED/MEDIASOUP_ERROR5 种 code → status 映射)src/middlewares/error-handler.ts(ZodError → 400 VALIDATION_ERROR / AppError → 对应 status / Fastify 4xx 透传 / 未知 → 500 INTERNAL_ERROR)src/mediasoup/codecs.ts(opus + VP8 + H264 三种RouterRtpCodecCapability)src/services/{router,transport,producer,consumer}.service.ts(4 文件,Map + observer-close 自清理;Consumer 还监听producerclose级联关闭)src/routes/{router,transport,producer,consumer}.route.ts(4 文件,9 接口均手动调.parse()+ 调用对应 service)src/app.ts注册 errorHandler + 以/internal/v1前缀挂载路由vitest.config.ts+tests/setup.ts+tests/{schemas,errors,app}.spec.ts+tests/services/{router,transport,producer,consumer}.service.spec.ts(7 个 spec 共 58 个测试)
- 检查点实测通过:
- ✅
typecheck/lint0 错误 - ✅
npm test:58/58 passed(约 900ms);覆盖率 stmts 80.89% / branches 76.03% / funcs 90.9% / lines 80.89%(>> 60% 目标) - ✅ 9 接口 happy path:
curl依次 POST /routers → POST /transports(send+recv)→ DELETE /routers/:id 全部 201/200 - ✅ 错误路径:unknown router→404 NOT_FOUND / produce on recv transport→409 CONFLICT / delete unknown producer→404 / resume unknown consumer→404 / lowercase roomCode→400 VALIDATION_ERROR(含 fieldErrors)/ 缺 token→401 UNAUTHORIZED
- ✅ 资源释放:DELETE router 后
routerMap.size === 0,observer.once('close') 同步清理 map
- ✅
- code-reviewer 子代理修复(2026-04-21):
- M1
_clearXxxMap→ 新增src/utils/test-guard.ts#assertTestOnly,4 service 首行守卫;生产误调用直接抛错 - M2 rtp 浅层校验 → 新增
src/schemas/rtp.ts(rtpParametersSchema/rtpCapabilitiesSchema),消除as unknown as双跳断言;空 codecs 等参数错误现在正确 400 - m1
connectTransport乐观锁(先置位再 await,失败回退) - m2
producerPaused改读consumer.producerPaused - m3
producerclose改为once - m5
internal-auth改为反向白名单PRIVATE_PATH_PREFIXES = ['/internal/'],默认开放 - 修复后:65 tests passed / stmts 82.87% / branches 75.83% / funcs 91.3%;
internal-auth.ts覆盖率 100%;schemas/rtp.ts覆盖率 100% - 其余 Minor / Nits(m4/m6
m10、n1n10)登记至CURRENT_STATUS.mdTask 16 收尾清单
- M1
- 工作量:2 人日(实际用时吻合估算;审查修复耗时 ~0.3 人日,已含在内)
Task 3:数据库 DDL + 模型 + DAO
- 目标:PostgreSQL 3 张表落地 + Go 侧 model/dao 完整实现
- 依赖:T0
- 主要产出:
deploy/docker/postgres/init.sql追加 3 张表 + 索引 + COMMENT(对齐设计 §5.1)app/meeting/model/{meeting_room,meeting_participant,meeting_chat}.goapp/meeting/dao/*.go(含 CRUD + 事务 + 按 code/user 查询 + 软删除等)app/meeting/constants/meeting_status.go(StatusPending/Active/Ended= 0/1/2)app/meeting/constants/meeting_role.go(RoleParticipant/Host/CoHost= 0/1/2,MVP 仅用 0/1)
- 检查点:
docker compose up postgres -d后表结构正确;可手动INSERT测试数据- DAO 单元测试:创建房间 + 参与者加入 + 主持人转让(事务)+ 列表查询
- 外键约束
ON DELETE CASCADE正常工作(删除房间自动清理参与者/聊天)
- 工作量:0.5 人日
Task 4:Go 侧 meeting 模块骨架 + Wire
- 目标:controller/service/router/provider 空壳搭建 + Wire 绑定完成
- 依赖:T3
- 主要产出:
app/meeting/controller/*.go(空 handler + 路由注册)app/meeting/service/*.go(空方法签名)app/meeting/service/interfaces.go(NotifyPusher/UserInfoResolver接口)app/meeting/router/meeting_router.goapp/meeting/provider/provider.go+wire_gen.goapp/provider/wire.go注册MeetingSet+ interface 绑定app/provider/provider.goApp结构体新增字段
- 检查点:
wire ./app/provider生成成功,编译通过go run cmd/server/main.go启动无报错- 路由打印包含
/api/v1/meeting/rooms等前缀
- 工作量:0.5 人日
Task 5:会议 REST 接口(创建/加入/离开/结束/列表/详情 + 邀请链接兑换 + 邀请)
- 目标:填充 Task 4 骨架中的业务逻辑,完整实现设计 §6.2 的 12 个接口
- 依赖:T4
- 主要产出:
- 会议号生成:
GenerateRoomCode()随机 9 位数字,冲突重试(< 3 次) - 密码 bcrypt 存取(
service.setPassword / verifyPassword) - 加入会议时校验:会议存在、未结束、容量未满、密码正确、用户未在其他会议
- 离开 / 结束:参与者表
left_at填入 + duration 计算 + 触发 mediasoup 资源清理 - 邀请链接 Token:
GenerateInviteToken() -> Redis SET EX 600,RedeemInviteToken(token)校验并删除 POST /rooms/:code/invite接受{invitee_ids, group_ids},生成 Token 后调NotifyPusher- DTO:
app/dto/meeting_dto.go完整定义请求/响应结构 - API 文档:
docs/api/frontend/meeting.md新建(全部 12 接口 + 示例)
- 会议号生成:
- 检查点:
- Postman 手测 12 接口全部 2xx;错误场景返回正确错误码(
meeting_not_found/meeting_full/password_incorrect等) - 容量限制:第 9 人加入返回
meeting_full - 密码连续错误 5 次锁定 10 分钟
- Postman 手测 12 接口全部 2xx;错误场景返回正确错误码(
- 工作量:1.5 人日
Task 6:WS 信令 11 事件处理器
- 目标:实现设计 §6.3 的 11 个 WS 事件,完整对接到
ws.Hub - 依赖:T4
- 主要产出:
app/meeting/controller/meeting_ws_handler.go:注册事件回调到 Hubapp/meeting/service/meeting_signal_service.go:3 组事件(房间 / 成员 / 媒体)的业务逻辑app/ws/hub.go接口扩展:MeetingSignalDispatcher接口注入 +DispatchMeeting(event, payload)方法app/meeting/constants/ws_events.go:11 个事件名常量- 权限校验:所有事件 handler 入口调用
assertIsParticipant/assertIsHost - 广播:
Hub.BroadcastToMeeting(roomCode, event, payload, excludeUserID)辅助方法
- 检查点:
- 通过
wscat或临时前端脚本连入 WS,逐个事件手测 - 未授权事件(非参与者发
meeting.member.state.changed)被拒绝 - 事件广播覆盖正确(excludeUserID 生效)
- 通过
- 工作量:1.5 人日
Task 7:Go → Node HTTP Client 封装
- 目标:实现设计 §6.6 的
NodeClient接口,挂接到 WS 信令流程 - 依赖:T2 + T6
- 主要产出:
app/meeting/service/node_client.go:9 个方法完整实现- 配置:
config.NodeServiceURL/config.NodeInternalToken(从 yaml + 环境变量) - 超时:HTTP Client 5 秒超时;关闭类操作 2 秒超时
- 重试:关闭类操作失败重试 2 次(指数退避 200ms/500ms)
- 日志:每次调用记录
funcName + room_code + duration + status_code - 错误映射:Node 5xx → 业务错误码
media_server_error;404 →media_resource_not_found;超时 →media_timeout
- 检查点:
- 单元测试:使用
httptest.NewServer模拟 Node,覆盖成功/失败/超时三类 - 集成测试:Go + Node 真实连通,创建 Router → Transport → Producer → 销毁链路
- 单元测试:使用
- 工作量:0.5 人日
Task 8:会议生命周期状态机(host 宽限期 + 自动转让 + 空房 TTL)
- 目标:实现设计 §6.5 的状态机完整逻辑
- 依赖:T5
- 主要产出:
app/meeting/service/meeting_service.go补充:OnHostDisconnect(ctx, roomCode)→ 写echo:meeting:host_grace:{code}EX 120sOnHostReconnect(ctx, roomCode, userID)→ 清除宽限期键HandleHostGraceExpired(ctx, roomCode)→ 转让主持(挑最早加入者)或销毁会议OnAllMembersLeft(ctx, roomCode)→ 设置房间 Redis key TTL 300sOnRoomTTLExpired(ctx, roomCode)→status=2, ended_reason=empty_ttl,清理 Node 资源
app/meeting/task/meeting_cleanup_task.go:每 30 秒扫描 Redis TTL + DB 状态,兜底清理- 事务包裹主持人转让(见设计 §11.3)
- 检查点:
- 手测:host 关闭浏览器 → 2 分钟内重连,身份保留 → 超过 2 分钟自动转让给另一成员
- 手测:全员退出 → 5 分钟后 DB
status=2、Redis key 清空 - 单元测试覆盖:转让事务回滚、并发转让竞态
- 工作量:0.5 人日
Task 9:前端 mediasoup-client 集成 + Pinia Store
- 目标:实现设计 §8.2 / §8.4 的 Store 状态与 mediasoup-client 生命周期封装
- 依赖:T2
- 主要产出:
frontend/src/utils/mediasoup-client.js:createDevice(rtpCapabilities)→mediasoupClient.DevicecreateSendTransport/createRecvTransport包装 + 事件桥接到 WSproduceAudio(track)/produceVideo(track, encodings)consume(producerId)全流程- 所有实例通过
markRaw包裹
frontend/src/store/meeting.js:- state 完整定义(见设计 §8.2)
- 10+ actions 与 WS
_on*事件响应
frontend/src/constants/meeting.js:事件名 / 类型 / 角色常量frontend/src/api/meeting.js:REST 封装frontend/src/utils/ws.js增加 meeting 事件分发桥
- 检查点:
- 手动启动前端,在控制台调用
useMeetingStore().createRoom({title: '测试'})→ 成功入会并推流 - 两个浏览器标签互相看见视频
- 关闭标签后另一端 1 秒内收到
meeting.member.left
- 手动启动前端,在控制台调用
- 工作量:1.5 人日
Task 10:前端设备预览页 + 创建页 + 加入页
- 目标:完成
/pages/meeting/{create,join,preview}.vue3 个页面 - 依赖:T5
- 主要产出:
MeetingCreate.vue:标题输入 + 密码 + 开关(入会静音 / 允许聊天) + 「立即开会」按钮MeetingJoin.vue:会议号输入(3-3-3 分组) + 密码 + 承接?code=xxx参数MeetingPreview.vue:- 左侧视频预览(
<video autoplay muted>承载 localStream) - 右侧设备列表(
enumerateDevices填充) - 音量条(AudioContext AnalyserNode 每 100ms 采样)
- 显示名称确认 + 「加入会议」按钮
- 左侧视频预览(
- 浏览器兼容性检测:不支持
getUserMedia→ 友好降级
- 检查点:
- Chrome / Firefox / Safari 手测设备选择 + 预览 + 音量条正常
- 手机浏览器(Safari iOS / Chrome Android)布局无错位
- 权限被拒绝时给出清晰引导
- 工作量:1 人日
Task 11:前端会议室主页(视频网格 + 工具栏 + 成员面板 + 响应式)
- 目标:实现
/pages/meeting/room.vue+ 6 个主要组件 - 依赖:T9
- 主要产出:
MeetingRoom.vue:主容器 + 网络质量监测 + WS 监听挂载VideoGrid.vue:响应式网格(见设计 §8.5)+ 按成员数动态布局VideoTile.vue:视频块(含静音图标、说话者流光占位)MeetingToolbar.vue:5 按钮(麦 / 摄 / 成员 / 聊天 / 挂断)+ 邀请入口MemberPanel.vue:成员列表 + host 菜单(静音 / 移除 / 转让)InviteDialog.vue:复制链接 + 复制会议号 + 选择联系人(复用 Phase 2a 组件)NetworkBadge.vue:3 档网络质量占位(具体动效 Task 15 打磨)
- 检查点:
- 4 人会议:UI 网格切换 1→2→3→4 人布局正常
- host 操作:静音他人 / 移除成员 / 转让主持 / 结束会议 全部成功
- 手机浏览器:单列主画面 + 缩略抽屉工作正常
- 工作量:2 人日
Task 12:会议内聊天面板
- 目标:实现
ChatPanel.vue+ 后端meeting_chats相关接口 - 依赖:T11
- 主要产出:
- 后端:
meeting_chat_controller.go+meeting_chat_service.go(2 接口:POST 发送、GET 历史分页) - WS 事件:
meeting.chat.new(广播给房间内所有成员) - 前端:
ChatPanel.vue(消息列表 + 发送框 + 滚动到底部) - Store 内
chat: {messages, hasMore}字段维护 - 清理任务:
meeting_cleanup_task每天扫描status=2 AND ended_at < NOW() - INTERVAL '24 hours',DELETE FROM meeting_chats WHERE room_id IN (...)
- 后端:
- 检查点:
- 两个用户互发 10 条消息,顺序正确
- 会议结束 24 小时后聊天记录被清理(可手动
UPDATE ended_at = NOW() - INTERVAL '25 hours'触发)
- 工作量:0.5 人日
Task 13:meeting_invite 通知对接
- 目标:打通
POST /rooms/:code/invite→ 通知中心 → 前端卡片跳转链路 - 依赖:T5
- 主要产出:
- 后端:
Invite()service 内调用notifyPusher.Push(ctx, receiverID, PushRequest{Type: "meeting_invite", Extra: MeetingInviteExtra{...}}) - 通知
extra结构补充(设计 §10.1) - 前端
NotifyItem.vue:meeting_invite卡片底部渲染「立即加入 / 稍后」按钮 - 前端路由跳转:「立即加入」→
/pages/meeting/preview?code=xxx;「稍后」→ 标已读 - 邀请链接复用:若
expired_at < now()灰显按钮提示"邀请已过期"
- 后端:
- 检查点:
- 用户 A 创建会议 → 邀请用户 B → 用户 B 通知中心铃铛出现未读 → TabBar「我的」红点亮起 → 点击卡片「立即加入」→ 成功入会
- 过期通知点击按钮 → 显示过期提示,不跳转
- 工作量:0.5 人日
Task 14:docker-compose 扩展 + 环境变量双态开关
- 目标:本机和公网双态部署脚本 + 文档
- 依赖:T11
- 主要产出:
deploy/docker-compose.dev.yml新增media-server服务 +coturn(profiles: [public]).env.example/.env.local.example/.env.public.example三份配置scripts/start.sh扩展media/full子命令scripts/stop.sh/scripts/status.sh同步scripts/deploy-public.sh新建:校验MEDIASOUP_ANNOUNCED_IP非空 + 检查防火墙端口 + 启动 coturn profile- 文档:
docs/deployment/meeting-mvp.md新建,涵盖本机 + 公网两种流程 + 常见问题
- 检查点:
- 本机:
scripts/start.sh full全量启动,5 分钟内全部服务就绪 - 公网(模拟):
MEDIASOUP_ANNOUNCED_IP=x.x.x.x docker compose --profile public up启动无报错 - 防火墙校验脚本能识别 UDP 40000-40199 未开放并给出提示
- 本机:
- 工作量:0.5 人日
Task 15:ui-ux-pro-max 定制 UI 打磨
- 目标:调用技能包产出 4 屏 EchoChat 原创风格,落地到代码
- 依赖:T11
- 主要产出:
- 调用:
npx openskills read ui-ux-pro-max+ 4 屏 brief(设计 §9.1) - 设计落地:
design-system/echochat/pages/meeting-home.mddesign-system/echochat/pages/meeting-preview.mddesign-system/echochat/pages/meeting-room.mddesign-system/echochat/pages/meeting-invite.md
- 代码落地:
VideoTile.vue说话者流光轮廓动效- 柔性网格:2/3 人布局的非等分样式
- 自视频浮窗四角吸附(
draggable+ 吸附计算) - 静音氛围色(工具栏底色绑定 computed
allMuted) NetworkBadge.vue3 档波浪动效
- 调用:
- 检查点:
- 视觉走查:4 屏与设计产物一致,色彩 / 留白 / 动效符合飞书简洁 + EchoChat 原创要求
- 动效性能:不影响 60fps(Chrome Performance 面板确认)
- 代码审查:
ui-ux-pro-max产物被真正使用,不留下"代码 vs 设计产物"脱节
- 工作量:2 人日
Task 16:E2E 验证 + Playwright MCP 回归 + 代码审查 + 文档同步
- 目标:端到端闭环验证 + 交付收官
- 依赖:T12 + T13 + T14 + T15 + T8
- 主要产出:
- Playwright MCP 场景脚本(保存到
.playwright-mcp/scenarios/):- 场景 1:创建会议 → 通过会议号加入第二个用户 → 互推音视频 → 结束
- 场景 2:邀请链接加入(含密码)
- 场景 3:通知中心「立即加入」按钮跳转
- 场景 4:主持人转让(主动)+ 主持人掉线(被动,2 分钟宽限)
code-reviewer子代理运行 → 出具报告,修复 P0/P1- 文档同步(设计 §十六 变更记录 + 以下文件):
docs/progress/CURRENT_STATUS.md:Phase 2e-2 状态改为 ✅docs/architecture/system-architecture.md:新增 meeting 模块docs/api/README.md+docs/api/frontend/meeting.md.cursor/rules/project-context.mdc:当前进度更新docs/plans/2026-04-20-phase2e-design.md:Phase 2e-2 段标记 ✅
- 测试报告:
test-report-phase2e-2-meeting.md落盘
- Playwright MCP 场景脚本(保存到
- 检查点:
- 4 个 E2E 场景全部通过
- 代码评审 0 个 P0/P1
- 所有文档链接可达、无 404
- 清理所有 TODO / FIXME /
console.log调试残留
- 工作量:1 人日
五、工作量汇总
| Task | 名称 | 工时 |
|---|---|---|
| 0 | mediasoup PoC Spike | 1.0 |
| 1 | media-server 项目骨架 | 0.5 |
| 2 | Node 侧 9 个 REST API | 2.0 |
| 3 | 数据库 DDL + 模型 + DAO | 0.5 |
| 4 | Go 侧 meeting 模块骨架 + Wire | 0.5 |
| 5 | 会议 REST 接口 | 1.5 |
| 6 | WS 信令 11 事件处理器 | 1.5 |
| 7 | Go → Node HTTP Client | 0.5 |
| 8 | 生命周期状态机 | 0.5 |
| 9 | 前端 mediasoup-client + Pinia Store | 1.5 |
| 10 | 前端预览/创建/加入页 | 1.0 |
| 11 | 前端会议室主页 | 2.0 |
| 12 | 会议内聊天面板 | 0.5 |
| 13 | meeting_invite 通知对接 |
0.5 |
| 14 | docker-compose + 双态配置 | 0.5 |
| 15 | ui-ux-pro-max 定制 UI 打磨 |
2.0 |
| 16 | E2E + 代码审查 + 文档同步 | 1.0 |
| 合计 | 17.0 |
说明:原 Phase 2e 路线图对 Phase 2e-2 的估算为 10-14 人日。当前 17 人日相较增加 3-7 人日的原因:
- 纳入了设备预览页(Task 10 的一部分,约 0.5 人日)
- 纳入了会议内聊天(Task 12,0.5 人日)
- 纳入了移动端响应式布局(分摊到 Task 11/15,约 1 人日)
- 纳入了邀请通知对接与 Phase 2e-1 的闭环(Task 13)
- 纳入了**
ui-ux-pro-max定制 UI 打磨**(Task 15,2 人日) - 预留了 PoC Spike 与 E2E 的显式工时(Task 0 / Task 16 共 2 人日)
该增量均是用户在 Plan 模式下的 11 项决策明确要求纳入的,已与设计文档 §2.2 对齐。
六、工作流与提交节奏
- 分支:
feature/phase2e-2-meeting-mvp(已由上游决策指定) - 每个 Task 完成后:
- 本地
go test ./app/meeting/.../pnpm test/npm run lint全绿 code-reviewer子代理小审(仅重要 Task 5/7/9/11 跑全量审)- 小粒度 commit:
feat(meeting): task-N <简短描述> - 不自动推远端,由用户显式触发
- 本地
- 每周节奏:Task 0-4 集中第一周(基础设施 + 骨架);Task 5-13 分散第二周(业务主体);Task 14-16 第三周(部署 + 打磨 + 收官)
- 阻塞处理:
- PoC(Task 0)如失败,立即暂停,与用户重议选型(livekit-server 替换 mediasoup)
- 任何 Task 工时超出估算 50% 以上,立刻停下来复盘,必要时拆子任务或推迟非关键部分
- Review Gate:每完成 3-4 个 Task 触发一次 review gate 汇报阶段进展
七、显式不做(本实施计划范围外)
- 不写 Phase 2e-3 相关代码(预约 / 提醒 / 等候室 / 锁定)
- 不写管理端 meeting 模块代码(推迟到 Phase 2f)
- 不做录制 / 屏幕共享 / 虚拟背景(二期)
- 不做多 Worker / 多 Node 实例 / Router Pipe(三期)
- 不引入 CI 流水线(接入后再由运维任务统一实施)
- 不做付费 TURN 服务接入(MVP 自建 coturn 即可)
八、关联文档
九、变更记录
| 日期 | 作者 | 变更 |
|---|---|---|
| 2026-04-21 | Agent | 首版落盘,17 个 Task 共 17 人日,含 PoC / UI 打磨 / E2E |
| 2026-04-21 | Agent | Task 0 mediasoup PoC Spike 完成:media-server/poc/ 跑通 2 浏览器互通,media-server/docs/poc-notes.md 归档压测数据与 7 项关键坑,技术栈选型锁定 |
| 2026-04-21 | Agent | Task 1 media-server 项目骨架完成:src 五件套(app/config/logger/worker/internal-auth)落盘;/healthz + /readyz + /internal/info 全部验收通过;Worker died 自动重启通过 kill -9 实测;Dockerfile 多阶段 + 非 root + HEALTHCHECK |
| 2026-04-21 | Agent | Task 1 Fastify 4 → 5 同日升级:fastify@4.28.1→5.8.5、fastify-plugin@4→5.1.0、@fastify/sensible@5→6.0.4、@fastify/websocket@10→11.2.0;src/app.ts 改用 loggerInstance: logger 消灭 pino 双实例;理由:Fastify 4 已过 2025-06-30 LTS 支持、v5 生态 GA、单实例回归;回归测试全部通过 |
文档结束