diff --git a/.cursor/rules/project-context.mdc b/.cursor/rules/project-context.mdc index 6e67029..5f543df 100644 --- a/.cursor/rules/project-context.mdc +++ b/.cursor/rules/project-context.mdc @@ -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` 级联关闭下游 consumer;Consumer 强制 `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,较首版 +2pp);9 接口 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/Nits(m4/m6~m10、n1~n10)登记至 Task 16 收尾清单 * **Task 3 ✅ Go meeting 模块数据库 DDL + Model + DAO 完成(2026-04-21)**:三张持久化表(`meeting_rooms` / `meeting_participants` / `meeting_chats`)落地 PostgreSQL,DDL 同时写入 `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×7(P2-1 `cleanupUserResources` 补 `transport:` 分支 + 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 避免闪 UI;P2-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 自动给 `