docs(phase2e-2): Task 16 收官 · 同步 6 份文档 + 落盘 test-report-phase2e-2-meeting.md

Phase 2e-2 会议 MVP 整体切 (Task 0-16 全部完成),本提交是 Task 16 最后一步
"文档同步"批次,配合历次 commit(cdaa39d P0 / ea2bf96 P1 / f5ae095 资源生命周期 /
5ed14c2 E2E 剧本 / c35097a P2+Nit)完成阶段收官。

同步范围:
- docs/progress/CURRENT_STATUS.md
  · 顶部元信息:"当前阶段"从"代码开发阶段 🚧"切为"已完成 "
  · 新增 2026-04-24 Task 16 收官小节:资源生命周期专项 / Playwright 5 场景 /
    P2+Nit 收尾 / 完成度汇总(Task 0-16 + 审查 4 P0 + 8 P1 + 7 P2 + 7 Nit)
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md
  · 状态切 ;Task 16 行切  + 实际交付小节(5 commits + 5 场景剧本 + 6 份文档)
- docs/plans/2026-04-20-phase2e-design.md
  · Phase 2e 顶层状态:2e-2 切 ;最后更新写明 Task 16 收官
- docs/reviews/2026-04-23-phase2e-2-code-review.md
  · (历次 commit c35097a 已追加)"Task 16 修复追踪"小节包含已修复表 + 推迟登记表
- .cursor/rules/project-context.mdc
  · Phase 2e 进度条更新;新增 Task 15 / 16 两个完整 bullet(Task 16 含 5 commit 详解)
- test-report-phase2e-2-meeting.md(新增)
  · 八章完整验证报告:测试环境 / 构建 / 功能 / E2E 剧本 / 代码审查修复 /
    部署双态 / 已知限制 / 验收结论

构建&清理复核(均在 Task 16 历次 commit 中验证通过):
- go vet ./... / go build ./... / npm run build:h5 / npx tsc --noEmit 全绿
- console.log / TODO / FIXME 扫描 0 残留

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-23 17:53:54 +08:00
parent c35097a0d8
commit 2452ca7332
5 changed files with 396 additions and 11 deletions

View File

@@ -27,7 +27,7 @@ alwaysApply: true
* 单端 WS 连接架构(沿用),不做多端已读同步(设计文档 §3.1/§3.5/§九 已修订,多端改造推迟到 Phase 2f/二期)
* 专用设计:`docs/plans/2026-04-20-phase2e-1-design.md`;实施计划:`docs/plans/2026-04-20-phase2e-1-implementation.plan.md`;验证报告:`test-report-phase2e-1-notification.md`
* API 文档:`docs/api/frontend/notify.md`
- 2e-2 会议 MVP约 17 天)🚧 **代码开发中**Task 0-14 ✅ / Task 15-16 待执行mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**host 宽限期 + 自动转让 + 空房 TTL+ 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机)
- 2e-2 会议 MVP约 17 天) **已完成**Task 0-16 全部 ✅2026-04-24 收官mediasoup Node.js 独立 `media-server/` + 即时会议≤8 人)+ 密码/邀请链接/通知邀请三合一 + 设备预览页 + 主持人四件套 + 会议内聊天 + **会议生命周期状态机**host 宽限期 + 自动转让 + 空房 TTL+ 双态部署(本机 + 公网 coturn+ 响应式(桌面/手机) + **code-reviewer 全栈审计**4 P0 + 8 P1 + 7 P2 + 7 Nit 闭环修复5 项登记推迟至 Phase 2f/3+ **资源生命周期专项**(前端 Pinia `_reset` 清理 broadcast timer + 后端 Redis `resourceTrackKey`/`memberStateKey` 显式 DEL + mediasoup Transport DELETE + lifecycle timer 统一撤销)+ **Playwright MCP 5 场景剧本 + 手工回归 checklist**`.playwright-mcp/task16/scenarios/`+ **test-report-phase2e-2-meeting.md** 落盘
* 专用设计:`docs/plans/2026-04-21-phase2e-2-design.md`16 章节);实施计划:`docs/plans/2026-04-21-phase2e-2-implementation.plan.md`17 个 Task
* 11 项关键决策已锁定D01-D11详见设计文档 §三
* **重要修订**`meeting_rooms.password` → `password_hash`bcrypt新增 `meeting_chats` 表 + `ended_reason` / `left_reason` 字段,新增 `echo:meeting:invite:{token}` / `host_grace:{code}` Redis key
@@ -36,6 +36,8 @@ alwaysApply: true
* **Task 2 ✅ 9 个内部 REST API 完成2026-04-21含代码审查修复**`media-server/` 落地 Router/Transport/Producer/Consumer 四类资源的 9 个接口,全部挂 `/internal/v1/*` 前缀;**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')` 自清理**`producerclose` 级联关闭下游 consumerConsumer 强制 `paused:true` 创建 + `/resume` 独立接口direction 强约束recv transport 拒 produce、send transport 拒 consume。**code-reviewer 子代理"有条件通过"**2 Major + 4 高价值 Minor 当场修复:(1) M1 `_clearXxxMap` 新增 `assertTestOnly` 守卫(生产误调用直接抛错);(2) M2 新增 `src/schemas/rtp.ts` 对 `rtpParameters` / `rtpCapabilities` 做 codecs 浅层校验mimeType/clockRate/payloadType 必填、codecs 数组 ≥1消除 `as unknown as` 双跳断言;(3) m1 `connectTransport` 改乐观锁(先置位再 await(4) m2 改读 `consumer.producerPaused`(5) m3 `producerclose` 改为 `once`(6) m5 `internal-auth` 改为反向白名单 `PRIVATE_PATH_PREFIXES = ['/internal/']`(默认开放)。**65 个 vitest 测试全过(~1s、覆盖率 82.87%/75.83%/91.3%/82.87%**stmts/branches/funcs/lines较首版 +2pp9 接口 happy path + 6 类错误路径人工 curl 全部按预期返回201/200/400/401/404/409。产出`src/schemas/*`6 文件,新增 `rtp.ts`+ `src/services/*`4 文件)+ `src/routes/*`4 文件)+ `src/middlewares/{error-handler,internal-auth}.ts` + `src/utils/{errors,test-guard}.ts` + `src/mediasoup/codecs.ts` + `vitest.config.ts` + `tests/*`8 spec 文件,含 `test-guard.spec.ts`)。余下 Minor/Nitsm4/m6~m10、n1~n10登记至 Task 16 收尾清单
* **Task 3 ✅ Go meeting 模块数据库 DDL + Model + DAO 完成2026-04-21**:三张持久化表(`meeting_rooms` / `meeting_participants` / `meeting_chats`)落地 PostgreSQLDDL 同时写入 `init.sql`(全量初始化)与 `phase2e2_migration.sql`幂等增量升级。Go 侧 `backend/go-service/app/meeting/{model,dao}` + 统一常量 `app/constants/meeting.go`3 个 model + 3 个 DAO`meeting_room_dao.go` 9 方法 / `meeting_participant_dao.go` 11 方法 / `meeting_chat_dao.go` 4 方法),共 24 个持久化方法;`JoinRoom` 事务内复用离会后的旧记录(`left_at=NULL,joined_at=NOW,duration=0`)避免审计表污染;`LeaveRoom` 用 `EXTRACT(EPOCH FROM (? - joined_at))::INT` 走 DB 时间防跨时区漂移;`TransferHost` 事务链(`role=1→0` + `role=0→1``FindActiveByUser` 用 JOIN 校验用户单点参会;`MarkEnded` 乐观锁防重复覆盖 `ended_reason``ListExpiredForCleanup` 供后续清理任务批量扫描。**关键风格修正(偏离实施计划草案)**:按 `project-context` 第 11 条「代码风格全局一致(最高优先级)」,常量归入 `app/constants/meeting.go` 单文件(与 `group.go`/`notify.go` 同构),而非草案的 `app/meeting/constants/*.go`;时间字段统一 `TIMESTAMP(0)` 取代草案的 `TIMESTAMPTZ` 对齐项目所有现有表;冗余 `idx_meeting_rooms_code` 移除(`room_code UNIQUE` 已自动建索引)。验证:`go build ./...` / `go vet ./...` / `ReadLints` 零错误psql 集成脚本跑通 8 场景CRUD + 双 UNIQUE 约束 + 主持人转让事务 + `duration=10s` 精确匹配 + CASCADE 清零)。延续项目 Go 侧"零 `_test.go`"风格(用代码审查 + psql 真库验证 + Playwright E2E 三层守护)
* **Task 4 ✅ Go meeting 模块 service/controller/router 骨架完成2026-04-21**`app/meeting/` 补齐 service/controller/router/provider 四件套,接口 → 实现按设计文档 §5.3 一一对齐。产出:(1) `service/interfaces.go` 定义 3 个外部依赖接口(`NotifyPusher` / `UserInfoResolver` / `OnlineChecker`),解耦 notify/contact/ws 模块避免循环依赖;`OnlineChecker.IsOnline` 签名与现存 `ws.OnlineService` 一致(返回单 `bool`(2) `service/meeting_service.go` 声明 `MeetingService` + 8 个 sentinel error + 17 个业务方法空实现,全部返回 `ErrNotImplemented`(3) `controller/meeting_controller.go` 12 个 Gin 处理器 + `responseNotImplemented`501+ `requireUserID` 辅助;(4) `router.go` 12 条路由挂载到 `/api/v1/meeting/*` 并统一套 `jwtAuth` 中间件;(5) `provider.go` 定义 `MeetingSet = wire.NewSet(DAO×3, Service, Controller)`(6) 全局 `app/provider/wire.go` 挂入 `meetingApp.MeetingSet` + 3 条 `wire.Bind``NotifyPusher→NotifyService` / `UserInfoResolver→FriendshipDAO` / `OnlineChecker→ws.OnlineService`(7) `app/provider/provider.go` `App` 加 `MeetingService/MeetingController` 字段;(8) `router/router.go` 调用 `meetingApp.RegisterRoutes`。**顺手修复存量 bug**`admin/provider.go` 补齐 `MessageManage{DAO,Service,Controller}` 三个 provider解决旧版 `wire` 重生成报"no provider found"的遗留问题。验证:`go build ./...` / `go vet ./...` / `wire ./app/provider` 全绿;`GIN_MODE=debug` 启动 server 日志打印全部 12 条 `[GIN-debug] ... meeting/controller.(*MeetingController).Xxx-fm`curl 无 token 打 3 条代表性路由均返回 401 `缺少认证信息`JWT 中间件生效
* **Task 16 ✅ E2E 总回归 + 代码审查修复 + 文档同步完成2026-04-24 收官)**Phase 2e-2 会议 MVP **整体切 ✅**。调用 `code-reviewer` 子代理对 Go meeting 模块 + media-server + frontend meeting 做一次重写版全栈审计,落盘 `docs/reviews/2026-04-23-phase2e-2-code-review.md`4 P0 + 8 P1 + 8 P2 + 11 Nit**26 项闭环修复 + 5 项登记推迟至 Phase 2f/3**。分 5 个 commit 落地:(1) `cdaa39d` P0×4媒体资源归属 Redis Set 校验 / CreateRoom Router 失败改 fail-closed 补偿 / 会议密码从 URL query 迁到 Pinia `draftJoinPayload` in-memory(2) `ea2bf96` P1×8`TransferHost` 事务合并 + `UpdateHost` 冗余移除 / `ListChatMessages`+`ListMyMeetings` 改走 DAO 游标分页 / `EndRoom` 加 `SELECT…FOR UPDATE` + 事务化快照 / `logs.DetachContext` 保留 trace_id 剥离 cancel / `_broadcastSelfState` 2 次指数退避重试 / `HandleHostGraceExpired` 独立 SETNX 处理锁消除 TTL 自然过期盲区 / `pushExistingRoomState` 改同步补推消除并行窗口);(3) `f5ae095` 资源生命周期专项(前端 `_pendingBroadcastTimers` 集中清理 + `exitEndedRoom` action + `onUnload` 兜底;后端 `cleanupRoomRedisResidual` 统一 DEL Redis key + `MeetingLifecycleService.OnRoomEnded` 中央清理钩子撤销 hostGrace/emptyTTL 定时器);(4) `5ed14c2` Playwright MCP 5 场景剧本 + 手工回归 checklist 落盘 `.playwright-mcp/task16/`(含 `.gitignore` 精细化:运行时产物默认忽略 + Markdown 剧本精确白名单放行);(5) `c35097a` P2×7+Nit×7P2-1 `cleanupUserResources` 补 `transport:<id>` 分支 + media-server 新增 `DELETE /internal/v1/transports/:id` + Go `MediaOrchestrator.CloseTransport`P2-2 `preview.vue` `previewSeq` 序号 + 200ms 防抖杜绝快速切设备竞态P2-3 `room.vue` `onLoad` redirectTo 后 `redirectingToJoin=true` + return 避免闪 UIP2-6 `generateUniqueRoomCode` 重试耗尽 Error + `ErrRoomCodeConflict`P2-7 `SendChatMessage` 服务端 utf8 rune 500 字符上限 + Redis INCR 滑动窗口 30 条/60s + 3 个新错误类型 HTTP 400 映射P2-8 新增 `MEETING_ENDED_REASON_KICKED` + label + 后端 `OnWSDisconnect` 硬编码 `"ws_disconnect"`→`constants.MeetingLeftReasonDisconnect`Nit splitn/ttl 中央化/CheckOrigin 白名单/doRequest timeout 5→10s + CreateRouter 重试 300ms 退避/REDIS_PASSWORD×redis.conf 联动校验/isPrivatePath 按 path 匹配剔除 query/inflight lock 走读确认 + 注释强化/`_onMemberLeft` vs `_onProducerNew closed=true` 粒度走读复核)。登记推迟 5 项P2-4 WS token 迁出 URL query、P2-5 `MeetingChatService` 拆分、docker-compose 200 端口收敛、`producer.appData` 校验、广播时间 RFC3339 统一。验证:`go vet ./...` / `go build ./...` / `npm run build:h5` / `npx tsc --noEmit` 全绿;`console.log / TODO / FIXME` grep 0 残留6 份文档同步:本文件 + `CURRENT_STATUS.md` + `implementation.plan.md` + 审查报告 + `phase2e-2-design.md` + 新增 `test-report-phase2e-2-meeting.md`
* **Task 15 ✅ UI 打磨 + 主持人权限四件套完成2026-04-23**6 项原创 UI 特色全部落地(说话者探测双源 RTP stats + WebAudio / `SelfVideoFloat` 桌面 280×180 恒浮窗 + 四角吸附 + 图钉切换 / `MeetingToolbar.isAllMuted` computed 蓝紫氛围色 / `VideoGrid` `tile-slide-in` 入会滑入动画 / `NetworkBadge` 3 条 SVG 波浪相位错开 / 4 屏 design-system 文档);主持人四件套(请他静音 / 请他开麦 / 转让主持人 / 踢出会议)通过 `MemberPanel.vue` + `store.muteMember`/`unmuteMember`/`transferHost`/`kickMember` + WS 广播 `meeting.member.state.changed` / `meeting.host.changed` / `meeting.member.kicked` 完整链路打通;被操作侧自动响应(`_onMemberStateChanged` 检测 `changed_by !== myUid` → `stopLocalAudio/Video` + `uni.showToast`)。**后续媒体层稳定性补丁Task 15 三轮手工联调修复)**(A) 成员列表 audio/video 图标常灰:后端 `meeting.member.state.changed` 广播范围改为"含自己"确保进房后收到历史状态;前端 `_onMemberStateChanged` 对尚未在 `participants` 表中的 user_id 建占位行容错 REST+WS 时序错乱;(B) 单向流:`mediasoup-client` `ensureSendTransport/ensureRecvTransport` in-flight Promise 锁防重复创建;`_ensureRecvAllExisting` 进房后遍历补订阅已存在 producers(C) 同用户二次入会成员列表缺失:`store._reset` 清空所有 Map/Set 避免旧会议残留覆盖;入口 guard `if (_engine) return` 去除,转为 `_cleanupMedia()` 重置。Playwright MCP 7 屏截图回归通过(`.playwright-mcp/task15/{01..07}-*.png`)。下一步 Task 16 E2E 总回归 + 代码审查 + 文档同步。
* **Task 14 ✅ docker-compose 扩展 + 环境变量双态开关完成2026-04-24**EchoChat 会议 MVP 正式具备**本机 Demo / 公网部署**双态一键化能力。产出:(1) `deploy/docker-compose.dev.yml` 新增 `media-server` 服务env 注入 `MEDIASOUP_ANNOUNCED_IP` / `MEDIA_INTERNAL_TOKEN` / RTC 端口段 40000-40199+ `coturn` 服务(`profiles: ["public"]` + `network_mode: host` + env 注入 realm / user / credential / 端口段 49152-65535`go-service` 新增 `depends_on: media-server`postgres/redis/minio 全部改为 env 变量可覆盖;(2) `deploy/.env.example`(总模板,涵盖 `DEPLOY_MODE/DB/REDIS/JWT/MINIO/MEDIA/TURN` 全字段)+ `deploy/.env.local.example`(本机 Demo 预填值,`MEDIASOUP_ANNOUNCED_IP=""` 空 = 自动内网 IP`TURN_ENABLED=false`+ `deploy/.env.public.example`(公网部署模板,所有敏感字段使用 `_REPLACE_WITH_STRONG_PASSWORD_` / `_REPLACE_WITH_YOUR_PUBLIC_IP_` / `_REPLACE_WITH_TURN_SECRET_` 占位符 + 部署前 checklist(3) `scripts/start.sh` 新增 `full` 子命令(`ensure_env_file` + `start_full` → `docker compose -f docker-compose.dev.yml --profile public up -d --build``scripts/stop.sh` 新增 `full` 子命令;`scripts/status.sh` 扩展检测 `echochat-go-service/media-server/coturn` 三个应用容器;(4) `scripts/deploy-public.sh`(新建,`chmod +x``step_validate_env``.env` 存在 + `DEPLOY_MODE=public` + `MEDIASOUP_ANNOUNCED_IP` 非空 + 所有 `_REPLACE_WITH_*_` 占位符已替换)→ `step_check_ports`(本机端口 + 云厂商安全组 checklist→ `step_check_docker`daemon + Compose V2→ `step_launch`(按 `TURN_ENABLED` 决定 `--profile public` 取舍 + `wait` media-server `/healthz`(5) `docs/deployment/meeting-mvp.md`(新建):本机 Demo + 公网双态部署全流程 + coturn 配置 + 强密码生成 + 防火墙 checklist + FAQ`python3/g++` / 视频问题 / `coturn` 网络模式 / HTTPS / 数据库备份)。**关键设计决策**(a) 双态开关 = 纯 env 差异(`MEDIASOUP_ANNOUNCED_IP` 空 vs 非空 + `TURN_ENABLED` true vs false(b) coturn 在 Linux 上必须 `network_mode: host`TURN 依赖大段随机 UDP 端口)+ `profiles: ["public"]` 隔离保证本地 Demo 不浪费资源;(c) `deploy-public.sh` 扫描 `_REPLACE_WITH_*_` 占位符防"示例值上生产"事故;(d) Compose V2 `--profile` 必须作为顶层 flag`docker compose --profile public -f x.yml config`(e) 不起独立 `docker-compose.prod.yml`profile + env 分离即可。验证:`docker compose config` 两种模式local / public均 `--quiet` 通过;`deploy-public.sh` 三场景(`.env` 缺失 / 含占位符 / 空 `ANNOUNCED_IP`)全部按预期退出 1完整正确配置下顺序通过三步校验进入 `step_launch`。下一步 Task 15 UI 打磨 + 主持人权限四件套
* **Task 13 ✅ meeting_invite 通知卡片对接完成2026-04-23**:后端 `MeetingService.InviteUsers` 的 `PushPayload.Extra` 补齐设计 §10.1 要求的 `inviter_id / inviter_name / inviter_avatar / expired_at`Unix 秒,与 Redis TTL `MeetingInviteTokenTTL=600s` 一致,取代之前只有 `room_code/room_title/has_password/invite_token` 的简化版本);前端 `constants/notify.js` 把 `supportsInlineAction` 扩展支持 `meeting_invite`,新增 `NOTIFY_INLINE_ACTION_LABEL` 映射表让按钮文案按 type 动态变化meeting_invite → `立即加入 / 稍后`friend_request → `同意 / 拒绝``NotifyItem.vue` 新增 `isExpired` 计算属性(纯客户端比对 `extra.expired_at * 1000 < Date.now()`),过期态合并为单个 disabled 的"邀请已过期"按钮 + `.notify-btn--expired` 灰显样式;`pages/notify/index.vue` 在 `handleAccept`/`handleReject`/`_navigateByNotify` 均增加 `NOTIFY_TYPE_MEETING_INVITE` 分支,新增 `_navigateToMeetingInvite(extra)` 跳 `/pages/meeting/preview?mode=join&code=xxx`,过期时 toast 提示不跳转,`handleReject`"稍后")只 `markRead` 不发接口。**附带修复commit 310ea03一个隐藏很深的 WS `.ack` 回归 bug**`frontend/src/services/websocket.js` 早期的 `_onMessage` 在命中 `.ack` 事件后 `_handleAck(msg); return;`,直接吞掉了 `_emit`Phase 2a 以后 `chat.js`/`meeting.js` 都在 `wsService.on('xxx.ack', ...)` 订阅 ACK 做"临时消息替换为真实消息",这条关键事件从未触发 → 发送方消息永远 `_sending=true`、前面显示 loading 圆圈、下方「已读/未读」标签永不渲染。修复:移除 `return`,改为同时走 `_handleAck`Promise `pendingAcks` 通路)+ `_emit`(订阅通路)。补强:`chat.js._appendMessage` 命中 `client_msg_id` 的临时消息时不再 dedup 丢弃,而是就地合并服务端字段并显式清 `_sending:false/_failed:false`,作为"丢 ACK 但不丢广播"场景的兜底。验证curl 双角色端到端(登录 testuser1/testuser2 → 创会 → 邀请 → 拉通知列表)返回 `type=meeting_invite` 且 `extra` 8 字段齐全,`expired_at = invited_at + 600` 与 Redis TTL 对齐用户手测「消息卡圈」bug 确认修复。Playwright UI 回归因多用户上下文环境限制暂缓到 Task 16 总回归统一做。下一步 Task 14 docker-compose 扩展 + 本机/公网双态部署
* **Task 10-12 ✅ + 2026-04-22 UI 打磨:前端会议主链路全部打通(含三轮 uni-button 遮罩 / z-index / toggle 深度修复)**:前端 `/pages/meeting/{create,join,preview,room,index}.vue` 五页全量落地;组件 `MeetingToolbar` / `VideoGrid` / `VideoTile` / `MemberPanel` / `InviteDialog` / `ChatPanel` 全部完工;后端 `SendChatMessage` + `chatToDTO` 补齐 `user_name` / `user_avatar`WS 推 + REST 列表均带真实昵称);`store/meeting.js` 新增 `cleanupStaleMeetings` + `createAndEnter/joinAndEnter` 首次静默 + 自动重试。**关键技术攻坚**(1) **uni-button `::after` 全视口遮罩(两轮)**uni-app H5 自动给 `<button>` 编译后的 `<uni-button>` 注入 `::after { position:absolute; inset:0 -1200px -80px 0 }` 作为点击反馈2400×160 超大遮罩MeetingToolbar 里最后一个"离开"按钮的 `::after` 横跨整个 toolbar 导致其他按钮全部点不响应、离开弹窗的"取消"也触发结束会议 → `.btn::after { content:none; display:none }` + `.btn { position:relative }` 修复ChatPanel 里 `.btn-send { all:unset }` 把 position 重置为 static 导致 `::after` 的 absolute 定位沿 DOM 树回溯到 `<body>` 作为 containing block 形成全屏遮罩,关闭/输入框/发送全被挡 → `.btn-send::after { content:none; display:none }` 修复。(2) **面板 z-index 冲突**`MemberPanel/InviteDialog .panel-root` 是 `position:fixed; inset:0; z-index:200` 全屏 mask打开时把 toolbar 遮掉,用户再次点击"成员/邀请"按钮命中的是 mask 而非按钮导致 toggle 永远不生效 → `.toolbar { z-index:210 }` 浮在 mask 之上(仍低于 `.leave-mask` 的 220。(3) **H5 全局容器偏左上**:桌面大窗口下 `uni-app`/`uni-page` 祖先默认不 100% 拉伸,导致 `position:fixed; inset:0` 的 `.room` 相对偏小祖先盒子定位 → `App.vue` 的 H5 条件块给 `html/body/#app/uni-app/uni-page*` 全部 `width/height:100%``.room` 加 `100vw/100vh/z-index:1000` 二道防御。(4) **刷新后残留会议静默清理**`joinAndEnter/createAndEnter` 首次走 `silent:true` 不弹 toast后端 `staleMeetingHint` 类错误自动调 `cleanupStaleMeetings()` + 重试一次;`leaveRoom({silent:true})` 抑制"你不在此会议中"toast。(5) **预览↔会议内视频统一 HD**`preview.vue` + `store/meeting.js#startLocalVideo` 统一 `width:{ideal:1280} height:{ideal:720} frameRate:{ideal:24,max:30}`。(6) **成员/邀请/聊天按钮 toggle**`openMembers/openInvite/openChat` 全改为取反,符合"再次点击关闭"直觉。验证Playwright 实测所有 toolbar 按钮 + close/textarea/send + 成员二次点击均 `hitSelf=true`,离开弹窗取消不再误触结束。相关提交:`cacf4ae` / `a311c03` / `3aa8174` / `ff46dc5` / `ce09315` / `c529ff4` / `0c9f048` / `16225c4` / `1222bf5` / `59f824a` / `eb3857a`。下一步 Task 13 主持人权限全链路。