Commit Graph

198 Commits

Author SHA1 Message Date
a17b002c2d 更新 deploy/.env.public.example 2026-05-15 18:33:42 +08:00
d9774ceb0e 更新 deploy/docker-compose.dev.yml 2026-05-15 17:50:02 +08:00
eba7f2b649 更新 deploy/docker-compose.dev.yml 2026-05-15 17:13:28 +08:00
69807dd235 更新 deploy/docker-compose.dev.yml 2026-05-15 17:05:26 +08:00
620b7bc130 更新 deploy/docker-compose.dev.yml 2026-05-15 16:52:16 +08:00
104bc7b9aa 更新 deploy/docker-compose.dev.yml 2026-05-15 16:47:46 +08:00
c886276229 更新 backend/go-service/config/config.dev.yaml 2026-05-15 16:42:33 +08:00
2846ce5e7d 更新 deploy/docker-compose.dev.yml 2026-05-15 16:39:19 +08:00
d1a3735acc 更新 backend/go-service/config/config.dev.yaml 2026-05-15 13:53:14 +08:00
5dcb506bda 更新 backend/go-service/config/config.dev.yaml 2026-05-15 12:54:02 +08:00
c469d35a61 更新 backend/go-service/config/config.dev.yaml 2026-05-15 12:50:20 +08:00
17d2d0a1db 更新 scripts/deploy-public.sh 2026-05-15 12:36:49 +08:00
a98773a5e0 更新 deploy/docker-compose.dev.yml 2026-05-15 12:19:00 +08:00
d03d4532ed 更新 media-server/.env.example 2026-05-15 12:13:01 +08:00
1ea6036714 更新 deploy/.env.public.example 2026-05-15 12:12:14 +08:00
1cdaeb887a 更新 scripts/start.sh 2026-05-15 12:09:22 +08:00
2485c1d8d2 更新 deploy/.env.public.example 2026-05-15 12:03:22 +08:00
26d3af787b 更新 media-server/.env.example 2026-05-15 11:19:13 +08:00
8292bf86e4 更新 frontend/vite.config.js 2026-05-14 20:40:04 +08:00
3b8cb6c274 更新 frontend/vite.config.js 2026-05-14 19:07:22 +08:00
d4c5f41952 更新 admin/vite.config.js 2026-05-14 19:06:15 +08:00
bujinyuan
f41e63ba6b docs(phase2e-2): 补齐顶层文档进度同步,Phase 2e-2 会议 MVP 全面切
用户准备开始真实用户测试,此 commit 对"六份文档同步"之外遗漏的上层/入口文档
做二次补齐,保证所有出现阶段状态描述的文档都与 CURRENT_STATUS.md 一致。

补齐范围:
- README.md
  · "MVP 功能规划" 原把 Phase 2 / Phase 3 标记为"待开始",与实际进度严重偏离
  · 整段重写为"开发进度"小节,按 Phase 1 / 2a-d / 2e-1 / 2e-2 分别列出 
    已完成状态与关键交付;2e-3 / Phase 3 / P2 推迟项独立列为"待启动(规划中)"
  · "文档导航"扩展 5 条新入口:Phase 2e 路线图 / 2e-2 设计 / 2e-2 实施 /
    2e-2 代码审查 / 2e-2 验收报告 / 会议 MVP 部署指南
- docs/plans/2026-02-27-echochat-system-design.md(整体系统设计方案)
  · §Phase 2e 行:追加"2e-1  / 2e-2  / 2e-3 📋 待启动"状态徽标
  · §Phase 2e-2:从"📋 设计阶段完成,代码开发待启动"改为
    " 已完成(2026-04-24,Task 0-16 全量落地 + 代码审查 4 P0 + 8 P1 + 7 P2 + 7 Nit 闭环)"
- docs/plans/2026-04-21-phase2e-2-design.md(Phase 2e-2 设计文档)
  · 顶部"状态 / 最后更新"由"📋 设计阶段(待评审后进入代码开发)"切 
  · §一 文档定位的"本文档"行同步切 
- docs/plans/2026-04-24-phase2e-2-task15-ui-polish.plan.md(Task 15 计划)
  · 顶部"状态"由"📋 设计阶段(等待用户 Review)"切 
  · 追加"交付记录"行指向 CURRENT_STATUS.md Task 15/16 交付条目
- docs/deployment/meeting-mvp.md(会议 MVP 部署指南)
  · 顶部元信息追加阶段  标记 + Task 16 追加的 REDIS_PASSWORD × redis.conf
    requirepass 联动校验指引

验证:workspace grep "📋 设计阶段|🚧 代码开发|待开始" 在顶层 README/docs 下
已无遗留进度描述与实际状态冲突(仅 README 新增的"Phase 2e-3 / Phase 3 — 待启动
(规划中)"章节为规划项,符合预期)。

Made-with: Cursor
2026-04-23 17:59:32 +08:00
bujinyuan
2452ca7332 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
2026-04-23 17:53:54 +08:00
bujinyuan
c35097a0d8 fix(meeting): Task 16 修复 code-reviewer 审计 P2 七项 + Nit 七项
覆盖 Phase 2e-2 代码审查报告(docs/reviews/2026-04-23-phase2e-2-code-review.md)
P2/Nit 收尾批次,均在本仓库完成闭环;剩余 5 项登记推迟至 Phase 2f/3。

===== P2 七项 =====
- P2-1 cleanupUserResources 补 transport 清理
  · media-server 新增 DELETE /internal/v1/transports/:id + transport.service.closeTransport
  · Go MediaOrchestrator 接口新增 CloseTransport;HTTP 实现按 doCloseRequest 走 4xx 幂等 + 指数退避
  · meeting_signal_service.cleanupUserResources 新增 "transport:<id>" 分支
- P2-2 preview.vue 快速切设备竞态
  · previewSeq 序号 + nextTick 后 stale 判断,丢弃过期结果
  · onVideoChange/onAudioChange 走 scheduleRestartPreview 200ms 防抖
  · onBeforeUnmount 清理 changeDebounceTimer
- P2-3 room.vue onLoad redirectTo 后补 return
  · 引入 redirectingToJoin 守卫,跳转页不再执行 onMounted 初始化
- P2-6 generateUniqueRoomCode 重试上限监控
  · 重试后成功:Warn 日志(码空间健康度告警)
  · 重试耗尽:Error 日志 + ErrRoomCodeConflict
  · 修正 logs.Error 调用签名(去掉多余的 nil)
- P2-7 SendChatMessage 服务端长度 + 频率限制
  · 新增 ErrChatContentEmpty / ErrChatContentTooLong / ErrChatRateLimited
  · utf8.RuneCountInString 校验 500 字符上限
  · Redis INCR + EXPIRE 滑动窗口(30 条/60s,首次写入 EXPIRE 兜底)
  · controller.handleError 映射为 HTTP 400
- P2-8 MEETING_ENDED_REASON_LABEL 覆盖复核
  · 新增前端专属常量 MEETING_ENDED_REASON_KICKED + 文案
  · store/meeting.js _onMemberKicked 使用常量
  · 同步修复后端 OnWSDisconnect 硬编码 "ws_disconnect" → constants.MeetingLeftReasonDisconnect
- P2 已修 P2-1/2/3/6/7/8;P2-4(WS token 迁出 URL query)与 P2-5(Chat 服务拆分)登记推迟

===== Nit 七项 =====
- Nit: kind:id 解析改用 strings.SplitN
  · cleanupUserResources / pushExistingRoomState 两处同步
- Nit: resourceTTL 中央化
  · 新增 constants.MeetingResourceTrackTTLSeconds(3600)
  · meeting_signal_service.go resourceTTL 由 const 改 var 并引用常量
- Nit: ws/handler.go CheckOrigin 白名单
  · NewHandler 新增 serverCfg 依赖;checkOrigin 支持同源放行 / dev 模式放行 / release 模式白名单严格匹配
  · config.ServerConfig 新增 WSAllowedOrigins(逗号分隔)+ AllowedOrigins() / IsRelease() 辅助方法
  · provider.go 新增 provideServerConfig,wire_gen.go 同步
- Nit: http_media_orchestrator.go 超时 + CreateRouter 重试
  · 默认 TimeoutMS 5000→10000ms 兼容 Worker 冷启动
  · 新增 CreateRouterRetry(默认 1 次,300ms 退避),仅对非 404 错误重试
  · config.dev.yaml / config.docker.yaml 同步写入显式配置
- Nit: deploy-public.sh REDIS_PASSWORD × redis.conf 联动校验
  · 检测 REDIS_PASSWORD 与 redis.conf 的 requirepass 配对一致性
  · redis.conf 增加公网部署 requirepass 使用说明
- Nit: media-server internal-auth isPrivatePath 按 path 匹配
  · 剔除 query/hash 后再与白名单 startsWith,避免 "?" 语义混淆
- Nit: mediasoup-client.js in-flight 锁走读确认
  · finally 分支已覆盖 resolve/reject 两路,追加注释强化语义
- Nit 走读复核:_onMemberLeft 整槽关闭 vs _onProducerNew(closed=true) 精确匹配 producerId
  · 粒度正确,无需改动(登记结论)

===== 构建验证 =====
- go vet ./... / go build ./... 通过
- frontend npm run build:h5 通过(仅 uni-app legacy warning,无 error)
- media-server npx tsc --noEmit 通过

===== 审查追踪小节 =====
docs/reviews/2026-04-23-phase2e-2-code-review.md 追加 "Task 16 修复追踪(2026-04-24 更新)":
- 已修复一览(本批次 14 处 + 历次 commit cdaa39d / ea2bf96 / f5ae095 / 5ed14c2)
- 推迟登记表(P2-4 / P2-5 / 端口收敛 / appData 校验 / RFC3339 时间格式,共 5 项)

Made-with: Cursor
2026-04-23 17:45:03 +08:00
bujinyuan
5ed14c2aac docs(meeting): Task 16 E2E 剧本落盘 + .gitignore 精细化
Playwright MCP 4 场景剧本(.playwright-mcp/task16/):
- README.md:总览 + 账号/环境/资源断言指南
- scenarios/01-create-and-join-by-code.md:创建→会议号加入→媒体打通→结束,含 Redis/MySQL/media-server 三维资源断言
- scenarios/02-invite-link-with-password.md:邀请链接带密码 + P0-4 "密码不走 URL query" 回归
- scenarios/03-notify-quickjoin.md:通知中心「立即加入」跳转 + 过期卡片降级
- scenarios/04-host-toolkit-and-transfer.md:主持人四件套 + 宽限期重连/到期自动转让(P1-1 原子性 + P1-7 处理锁验证)
- scenarios/05-reopen-5times-resource-cleanup.md:反复开关 5 次 + 前端 heap 快照/ Redis 累积对比/ media-server 观测端点对比,验证 Step A(f5ae095)资源清理修复

手工回归点(manual-regression.md):17 项 MCP 难以稳定覆盖的交互场景
(HTTPS 自签 / 跨网段 iOS / 网络抖动 / 物理返回 / 跨 tab 并发 / 摄像头抢占 等)

.gitignore 调整:
- 放行 .playwright-mcp/task16/ 下的 Markdown 剧本文档进入版本控制
- 仍忽略运行时产物(*.log/*.png/*.yml/截图)
- task15/ 保持未跟踪状态,避免历史截图噪声入库

Made-with: Cursor
2026-04-23 17:21:42 +08:00
bujinyuan
f5ae095033 fix(meeting): Task 16 会议销毁路径资源清理专项
前端修复:
- _onRoomEnded 保留结束页数据,新增 exitEndedRoom() 动作
  room.vue onUnload 在 ENDED 状态调用,释放 currentRoom / participants / chatMessages 等 pinia state
- endMeeting() API 成功后立即触发 _onRoomEnded 本地兜底,不再依赖 room.ended 广播
  避免 API 成功但 WS 抖动导致 engine / timer / AudioContext 泄漏
- 新增 _pendingBroadcastTimers 集合收纳 _broadcastSelfState 的 setTimeout 句柄
  _cleanupMedia 统一 clearTimeout,避免会议结束后 stale 重试逻辑排队

后端修复:
- MeetingLifecycleService.OnRoomEnded 统一生命周期收尾
  取消 graceTimers / emptyTTLTimers + Pipeline DEL host_grace / empty_ttl / 两个 handling 锁 key
- EndRoom 事务提交后遍历 activesBefore 调 cleanupRoomRedisResidual
  批量 DEL 每个前任活跃成员的 resourceTrackKey + memberStateKey
  补调 lifecycleSvc.OnRoomEnded 清理自身 timer + keys
- HandleEmptyRoomExpired MarkEnded 后补调 cleanupRoomRedisResidual + 清 host_grace 残留

验证:go vet + go build + npm run build:h5 全部通过

Made-with: Cursor
2026-04-23 17:12:38 +08:00
bujinyuan
ea2bf96c2f fix(meeting): Task 16 修复 code-reviewer 审计 P1 八项
按 docs/reviews/2026-04-23-phase2e-2-code-review.md 清单落地 P1 批次:

- P1-1 主持人转让原子性:MeetingParticipantDAO.TransferHost 内部事务
  合并 meeting_rooms.host_id 更新,service/lifecycle 移除冗余 UpdateHost
- P1-2 ListChatMessages / ListMyMeetings 回归 DAO:新增
  MeetingChatDAO.ListByRoomBefore 反向游标,service 移除 s.db 直查
- P1-3 ListMyMeetings N+1 放大优化:新增
  MeetingParticipantDAO.ListJoinedRoomsByUser JOIN + DISTINCT 单次 SQL
- P1-4 EndRoom 行锁 + 事务:s.db.Transaction 包裹
  SELECT FOR UPDATE → 快照 → MarkEnded → LeaveAllActive;DAO 补
  WithTx(*gorm.DB) 和 GetByIDForUpdate
- P1-5 后台 goroutine trace_id 保留:新增 logs.DetachContext(ctx);
  替换 meeting_service / meeting_signal_service 共 10 处 context.Background();
  meeting_ws_handler 每条 WS 消息分配独立 trace_id
- P1-6 _broadcastSelfState 重试:最多 2 次指数退避(700ms→2100ms),
  检测 localAudioEnabled/localVideoEnabled 已被后续动作覆盖时放弃旧 patch
- P1-7 HandleHostGraceExpired / HandleEmptyRoomExpired 处理锁:
  独立 host_grace_handling:<code> / empty_ttl_handling:<code> SETNX + 60s TTL,
  消除 Redis 自然过期 + 本地 timer 到点 + DEL 返回 0 的盲区
- P1-8 pushExistingRoomState 同步化:OnRoomJoin 返回前完成补推,
  消除与 REST member.joined 并行造成的前端状态闪烁

并附带修复 .gitignore 中 logs/ 规则误伤 pkg/logs/ 代码目录的历史遗留问题,
将 logger.go / trace.go 正式纳入版本控制。

验证:backend go build + go vet 通过;frontend npm run build:h5 通过。

Made-with: Cursor
2026-04-23 16:53:30 +08:00
bujinyuan
cdaa39d686 fix(meeting): Task 16 修复 code-reviewer 审计 P0 四项 + 落盘审查报告
code-reviewer 子代理(docs/reviews/2026-04-23-phase2e-2-code-review.md)对 Phase 2e-2
整体做全栈审计,暴露 P0 × 4 / P1 × 8 / P2 × 8 / Nit × 11。本次先修 P0:

- P0-1 / P0-2 媒体资源归属越权:
  新增 assertOwnsResource(roomCode, userID, kind, id) helper,用 resourceTrackKey Redis Set
  SIsMember 校验 transport/producer/consumer 归属;统一接入 OnTransportConnect /
  OnProduceStart / OnConsumeStart / OnConsumeResume / OnProducerClose 五个信令入口,
  一次封死同类横向越权漏洞(Redis 查询失败时 fail-closed,避免抖动开口子)。
  新增 service.ErrResourceNotOwned 错误常量。
- P0-3 CreateRoom Router 失败吞错:
  旧实现 mediaErr != nil 仅 logs.Warn 继续返回成功 → zombie 房间 + 入会必败 +
  一人一会议名额被占。改为 fail-closed 补偿:LeaveRoom + MarkEnded(system_error) +
  返回新 service.ErrMediaServiceUnavailable,controller.handleError 映射为 500。
- P0-4 会议密码明文走 URL:
  join.vue onNext 旧逻辑把密码拼 ?password=xxx,浏览器历史 / DevTools 留痕。
  新增 meetingStore.draftJoinPayload 内存态草稿,join.vue 只跳转
  ?mode=join&code=xxx,preview.vue onLoad 读取后立即 draftJoinPayload = null 清空,
  邀请链接里的 password 参数一律忽略。

验证:
- cd backend/go-service && go build ./... 通过
- cd frontend && npm run build:h5 通过
- 仅剩 1 个预先存在 unusedparams warning,与本次改动无关

P1 / P2 / Nit 清单保留在审查报告,按顺序继续处理。

Made-with: Cursor
2026-04-23 16:34:13 +08:00
bujinyuan
790996feaf feat(meeting): Task 15 UI 打磨 + 主持人四件套 + 媒体层稳定性回归修复
- Task 15 UI:6 项原创特色(说话者流光 / 柔性网格 / 自视频浮窗 / 静音氛围色 / 入会滑入 / NetworkBadge 3 条波浪)+ 说话者双源探测(RTP audioLevel + WebAudio RMS)+ 主持人四件套(静音/开麦/转让/踢出)
- 新增 SelfVideoFloat 浮窗组件 + 4 份 design-system 页面文档(home / preview / room / invite)
- 媒体回归补丁(手工联调触发):
  * 后端 signal_service 在 OnRoomJoin 追加 pushExistingRoomState → 向新加入者补发历史 producers + 历史成员 audio/video 状态
  * 新增 Redis Hash memberStateKey 持久化成员 audio/video enabled,OnMemberStateChanged 落盘、cleanupUserResources 清理
  * 前端 _broadcastSelfState 在本地音视频开关末尾同步 state.changed;_afterJoined 先 getRoom 再 room.join 修复后入者成员状态时序;_onMemberStateChanged 占位兜底
  * _cleanupRemoteProducer 改为按 producerId 精准清理(修复 "关音频误关视频")
  * _onRoomEnded + createAndEnter/joinAndEnter 强化 reset(修复重入会看不到自己画面)
  * mediasoup-client ensureSend/RecvTransport 引入 in-flight Promise 锁防并发重复建连
- 文档同步:CURRENT_STATUS.md / Task 15 plan / phase2e-2 implementation plan

Made-with: Cursor
2026-04-23 16:04:46 +08:00
bujinyuan
46b37b8b4c docs(meeting): 补齐 frontend/meeting.md 后续任务关联至 Task 14
- Task 9 补完完整描述(2026-04-21 前端 mediasoup-client 接入 + resume 走 WS 路径)
- Task 10-12 补入(2026-04-22 前端主链路 5 页 + 6 组件落地)
- Task 14 补入(2026-04-24 docker-compose 双态部署,API 契约无变)

Made-with: Cursor
2026-04-22 18:06:50 +08:00
bujinyuan
6913b9e72f docs(task14): 同步 Phase 2e-2 Task 14 进度至全量项目文档
- README.md:脚本表格新增 deploy-public.sh + full 子命令说明;
  "方式二" 改为 scripts/start.sh full + 指向 docs/deployment/meeting-mvp.md
- docs/api/README.md:meeting 模块状态标注 Task 5/6/7/13 + extra 8 字段
- docs/architecture/system-architecture.md:meeting 状态升至 Task 0-14 ;
  media-server / coturn 行同步标注 Task 14 双态编排落地
- docs/plans/2026-04-20-phase2e-design.md:最后更新 + 变更记录新增 04-24 条目
- media-server/README.md:后续任务新增 Task 9  + Task 14 ,指向部署指南

保证所有项目级 README / 架构 / 路线图文档都处于 Task 0-14  最新状态。

Made-with: Cursor
2026-04-22 18:04:20 +08:00
bujinyuan
a01e973335 feat(deploy): Task 14 完成 docker-compose 扩展 + 环境变量双态部署
- deploy/docker-compose.dev.yml 新增 media-server 服务 + coturn(profiles:public),
  全部服务参数改为 env 变量注入,go-service 依赖 media-server
- deploy/.env.example / .env.local.example / .env.public.example 三份模板,
  公网模板用 _REPLACE_WITH_*_ 占位符防"示例值上生产"
- scripts/start.sh / stop.sh / status.sh 新增 full 子命令对接 docker compose --profile public
- scripts/deploy-public.sh 新建:env 校验 + 端口 checklist + Docker 自检 +
  --profile public up -d --build + 健康检查闭环
- docs/deployment/meeting-mvp.md 新建:双态部署全流程 + 强密码生成 + 防火墙 + FAQ
- 双模式 docker compose config 校验通过;deploy-public.sh 三种错误场景按预期退出

同步更新 CURRENT_STATUS / implementation.plan / project-context
Task 0-14  / 剩余 Task 15-16

Made-with: Cursor
2026-04-22 17:59:48 +08:00
bujinyuan
ba8c72aae0 docs(task13): 补齐 meeting/notify API 文档 + Playwright UI 回归结果
## API 文档(Task 13 前漏补)
- docs/api/frontend/meeting.md §9 邀请用户接口:展开 extra 8 字段清单
  (room_code / room_title / has_password / invite_token / inviter_id
   / inviter_name / inviter_avatar / expired_at),并说明前端如何据
   expired_at 判过期
- docs/api/frontend/meeting.md 后续任务关联:Task 13 标  + deep-link 路径
- docs/api/frontend/notify.md 概览:
  * "支持内联按钮的通知类型" 表(friend_request / meeting_invite)
  * "meeting_invite 专用 extra 字段" 详细清单

## Playwright UI 回归结果(2026-04-23 补做)
采用 "A 侧 curl 邀请 + B 侧 Playwright 登录" 策略:
- testuser1 邀请 testuser2 → 通知中心渲染 meeting_invite 卡片
- 新邀请卡片 → 双按钮「立即加入 / 稍后」
- 过期邀请卡片 → 合并为单 disabled「邀请已过期」按钮 
- 点「立即加入」→ 跳 /pages/meeting/preview?mode=join&code=516-162-828 
- preview 页正确渲染 "即将加入会议 516-162-828" + 设备面板 
- 截图:task13-notify-meeting-invite-cards.png
        task13-preview-page-after-invite-accept.png
- CURRENT_STATUS.md + implementation.plan.md 已同步补录

Made-with: Cursor
2026-04-22 17:34:29 +08:00
bujinyuan
d6fb7ce291 feat(meeting): Task 13 完成 meeting_invite 通知卡片对接
## 后端
- MeetingService.InviteUsers 的 PushPayload.Extra 补齐设计 §10.1 要求字段:
  inviter_id / inviter_name / inviter_avatar / expired_at(Unix 秒,
  与 Redis TTL MeetingInviteTokenTTL=600s 对齐)

## 前端
- constants/notify.js:supportsInlineAction 扩展支持 meeting_invite;
  新增 NOTIFY_INLINE_ACTION_LABEL 映射(meeting_invite → 立即加入/稍后)
  + NOTIFY_INLINE_ACTION_DEFAULT 兜底
- components/notify/NotifyItem.vue:
  * 按 type 动态渲染按钮文案(actionLabel 计算属性)
  * isExpired 计算属性:meeting_invite 比对 extra.expired_at * 1000
  * 过期态合并为单个 disabled 的"邀请已过期"按钮
  * 新增 .notify-btn--expired 灰显样式
- pages/notify/index.vue:
  * handleAccept / handleReject / _navigateByNotify 增加 meeting_invite 分支
  * 新增 _navigateToMeetingInvite 辅助:跳 /pages/meeting/preview?mode=join&code=xxx
  * 过期时 toast "邀请已过期"不跳转,"稍后"仅 markRead 不发接口

## 文档
- docs/plans/2026-04-21-phase2e-2-implementation.plan.md:Task 13 标  + 详细产出
- docs/progress/CURRENT_STATUS.md:新增 Task 13 专节(关键技术点 / 验证记录 / 下一步)
- .cursor/rules/project-context.mdc:2e-2 进度刷 Task 0-13 

## 验证
后端 curl 双角色端到端(testuser1 创会 → 邀请 testuser2 → 拉通知列表):
- type=meeting_invite,extra 8 字段齐全
- expired_at = invited_at + 600,与 Redis TTL 对齐

Made-with: Cursor
2026-04-22 17:26:31 +08:00
bujinyuan
310ea03ce1 fix(im): 修复发送方消息一直卡在"发送中"圆圈、下方不显示已读/未读的 Bug
现象
----
在单聊里发送新消息后,本人气泡左侧一直显示 loop 圆圈(_sending=true),
气泡下方看不到"已读 / 未读"标签;但对端能正常收到消息。刷新页面、
走 fetchHistory 之后才能恢复正常显示。

根因
----
1. services/websocket.js 的 _onMessage:一旦发现 event 以 ".ack" 结尾,
   就走 _handleAck(seq→pendingAcks) 处理完后 **直接 return**,不再 _emit。
   这样通过 on('im.message.send.ack') / on('im.message.read.ack')
   订阅 ACK 的 listener(chat store 的 _onSendACK / _onReadACK)
   **永远收不到回调**,导致 _sending 标记一直没被清、id 没被合入,
   模板里 getReadLabel() 因为 !msg.id / _sending=true 返回空字符串。
2. store/chat.js 的 _appendMessage:按 client_msg_id 命中旧临时消息时,
   只是 isDup=true 直接 return,把服务端权威字段(包括 id)丢弃了,
   没有形成有效的兜底路径。

修复
----
- websocket.js: .ack 报文在 _handleAck 之后 **继续走 _emit**,让 Promise
  通路(sendWithAck → pendingAcks)与订阅通路(on(...ack))并行分发,
  互不冲突。这是真正解决"卡圈"的主修复。
- chat.js _appendMessage: 把 dedup 改成 "就地合并" 语义:
  * 同 id 命中 → 真重复丢弃(不回退)
  * 同 client_msg_id 命中本地 _sending 临时消息 → 用服务端字段
    覆盖 id/created_at/..., 同时清 _sending/_failed
  * 其余情况才 push 新条
  作为广播帧先于 ACK 到达时的二级兜底,也保证后续再有 ACK/广播
  重复到达不会产生重复气泡。

影响面
------
- 单聊/群聊 发送消息后"已读 / 未读 / N人已读"标签恢复实时显示
- im.message.read.ack(对方读到某条)也能正常驱动 readStatusMap 更新
- meeting 等模块的 sendWithAck() 不受影响(走的是 pendingAcks Promise
  通路,本次只是在此之后多了一个 _emit 分发,对业务无副作用)

Made-with: Cursor
2026-04-22 17:05:31 +08:00
bujinyuan
ea28549a5f feat(scripts): start.sh 启动完成时自动展示前端 / 管理端局域网访问地址
Vite dev server 已绑定 host: '0.0.0.0' 支持局域网访问,但原来的 start.sh
只在 summary 里打印 localhost 地址,Vite 输出的多条 Network URL
被埋在日志里用户看不到。这里扩展 print_summary:

- 新增 wait_and_collect_network_urls(log, timeout):等待日志出现
  "Network:" 行(最多 N 秒),然后从中抽取所有 http://LAN_IP:PORT URL
  并去重(排除 127.0.0.1 回环)
- summary 输出最后追加「局域网访问地址」分块,分别展示前台 / 管理端
  在当前机器所有网络接口上的可达地址
- 附注提示:后端 API(8085) / 媒体 (3300) 的跨设备访问需要在
  前端 .env 改 baseURL

这样在同 WiFi 的手机 / 其它设备上可以直接从启动日志里拿到可用入口,
无需再去 .run/logs/frontend.log 里手动 grep。

Made-with: Cursor
2026-04-22 16:50:43 +08:00
bujinyuan
d218b84843 docs(phase2e-2): 同步 Task 10-12 + UI 打磨进度至 CURRENT_STATUS / 实施计划 / project-context
- CURRENT_STATUS.md:
  * 头部"最后更新"升级到 2026-04-22
  * 新增完整段落记录 Task 10-12 + 一轮深度 UI 打磨
    (uni-button ::after 全视口遮罩 / toolbar z-index 提升 /
    面板 toggle / 全局 H5 容器铺满 / 静默 stale 清理 /
    视频 HD / 会议聊天真实昵称)
  * 附 Playwright 验证结论与相关 commit 列表

- 2026-04-21-phase2e-2-implementation.plan.md:
  * 顶部状态改为"代码开发中(Task 0-12  / Task 13-16 待执行)"
  * 新增 16 行进度看板表格 + 2026-04-22 UI 打磨专项要点
  * Task 10/11/12 header 标注  完成日期

- .cursor/rules/project-context.mdc:
  * 当前进度条目更新为"Task 0-12 "
  * 新增 Task 10-12 + UI 打磨的独立条目
    完整收录三轮攻坚(uni-button 遮罩两次 + z-index +
    全局容器 + stale 静默 + HD + toggle)的技术细节

Made-with: Cursor
2026-04-22 16:45:56 +08:00
bujinyuan
cacf4ae074 fix(meeting): 提升 toolbar z-index 确保成员/邀请面板开启时仍可点击
MemberPanel 和 InviteDialog 的 .panel-root 使用了 position:fixed; inset:0;
z-index:200 的全屏遮罩,当面板打开时会覆盖整个视口,包括底部 toolbar。
这导致:
- 用户再次点击 toolbar 的"成员"/"邀请"按钮时,命中的是 mask 而非按钮
- mask 的 @click.self 虽然也会关闭面板(通过点击空白区域),
  但与 toolbar 按钮 toggle 的用户预期行为不一致

修复:把 MeetingToolbar .toolbar 的 z-index 从 15 提升到 210,
确保其高于 .panel-root (z:200) 而低于 .leave-mask (z:220)。

验证:Playwright 实测点击成员按钮 hitSelf=true,打开后再次点击正常关闭。

Made-with: Cursor
2026-04-22 16:39:41 +08:00
bujinyuan
a311c03a21 fix(meeting): 聊天面板点击失效 & 成员/邀请按钮支持 toggle
ChatPanel 的 .btn-send 使用了 all:unset 导致 position 变为 static,
其 uni-app 自动注入的 ::after 伪元素(inset: 0 -1200px -80px 0)
absolute 定位会回溯到 body 作为 containing block,
形成一个覆盖几乎整个视口的隐形遮罩层,导致聊天面板的关闭按钮 (.btn-close)、
输入框 (textarea) 甚至发送按钮本身周围区域都无法点击/输入。

修复:
1. ChatPanel 的 .btn-send::after 从仅 border:none 升级为
   content:none + display:none,彻底抹除遮罩
2. room.vue 中 openMembers / openInvite 改为 toggle 切换,
   解决"再次点击成员按钮面板不会关闭"的问题

验证:Playwright 真实 DOM 命中测试 close/textarea/send 全部 hitSelf=true,
成员按钮二次点击面板正常关闭。

Made-with: Cursor
2026-04-22 16:34:30 +08:00
bujinyuan
3aa8174bc0 fix(meeting): 抹除 uni-button ::after 遮罩修复工具栏点击失效
uni-app H5 会给每个 <button> 编译后的 <uni-button> 自动注入一个
::after 伪元素作为点击态反馈遮罩,其 inset 为 0 -1200px -80px 0
(即 2400px × 160px),向按钮右侧扩展 1200px,下方扩展 80px。

这导致会议工具栏(MeetingToolbar)里每个按钮的 ::after 都横跨整个
toolbar,最后一个"离开"按钮 DOM 顺序最上,其 ::after 覆盖了所有
兄弟按钮的命中区域:点击"静音/视频/邀请/成员/聊天"时实际都命中
"离开"按钮的 ::after,触发离开弹窗;且弹窗中的"取消"按钮同样被
右侧"结束会议"按钮的 ::after 遮挡,点击取消反而会触发结束会议。

修复:在 .btn 和 .leave-btn 上显式设置 ::after { content: none;
display: none },并把按钮设为 position: relative 固化堆叠上下文。

验证:Playwright 真实点击各按钮 hitSelf=true,取消按钮不再
错误触发结束会议逻辑。

Made-with: Cursor
2026-04-22 16:27:16 +08:00
bujinyuan
ff46dc5f76 fix(phase2e-2): 视频 HD 分辨率 + toolbar 防溢出 + 聊天按钮 toggle
问题
1. 进入会议后视频比预览页更"近、更糊"
   · 预览 / startLocalVideo 均固定 640x480,mediasoup 视频轨清晰度偏低
   · 进房后 videoTile cover 裁切在大画布更明显 → 观感"放大"
2. 会议内聊天 / 邀请 / 成员按钮"点击无反应"
   · 核心:.body flex 子内容过高时 toolbar 未固定收缩,被挤出视口
     下方 (y > vh),用户点击位置在屏幕外
   · 次要:openChat 仅 set true,已打开状态下再次点击无视觉反馈
3. 聊天按钮无法关闭面板:只有面板内 × 按钮能关

修复
- store/meeting.js startLocalVideo + preview.vue getUserMedia 统一
  改为 ideal 1280x720 + frameRate ideal 24 / max 30,浏览器在设备不
  支持时自动降级;进房与预览视觉表现一致
- components/meeting/MeetingToolbar.vue .toolbar 加 flex-shrink:0
  + position:relative + z-index:15,保证 toolbar 永远贴底不被 body
  压缩,且点击层级高于 ChatPanel 背景
- pages/meeting/room.vue .top-bar 加 flex-shrink:0,避免 body 溢出
  时顶栏压缩引发整体错位
- pages/meeting/room.vue openChat 改 toggle:已打开则关闭,未打开则
  展开并 markChatsRead,交互更符合直觉

验证(Playwright 1024x576)
- 预览 getVideoTracks()[0].getSettings() → 1280x720 / 24fps ✓
- 进房后 localVideo settings 同样 1280x720 ✓
- 聊天按钮 boundingRect y=505 bottom=567 ≤ vh 576,inViewport=true ✓
- 点击聊天 → chat-col.open=true 宽度 320.5px;再点击 → open=false ✓
- 截图 room-hd-chat-open.png:视频清晰、聊天面板展开、toolbar 贴底

Made-with: Cursor
2026-04-22 16:13:25 +08:00
bujinyuan
ce09315fa6 feat(phase2e-2): 会议首页落地 "创建 / 加入" 双入口
问题
- tabBar "会议" 指向 pages/meeting/index,但该页仍是占位的
  "视频会议功能开发中…",用户从底部 tab 进入无入口按钮,
  必须手改 URL 到 /pages/meeting/create 或 /join 才能进会议

修复
- 重写 pages/meeting/index.vue 为真实的 Meeting Hub:
  · 顶部 hero 标题 + 描述
  · "创建即时会议" 主按钮(蓝色渐变卡片,跳 /pages/meeting/create)
  · "加入会议" 次按钮(卡片,跳 /pages/meeting/join)
  · 提示文案说明入会流程与主持人能力
- 保留 CustomTabBar 底栏 current=2

验证
- Playwright 截图确认布局、两个入口按钮视觉与点击区域均正常

Made-with: Cursor
2026-04-22 16:04:06 +08:00
bujinyuan
c529ff4689 fix(phase2e-2): 全局 H5 容器铺满视口 + 静默自动清理 stale 会议
问题
1. 桌面浏览器大窗口下所有页面内容偏左上、右下大片空白:
   uni-app H5 的 html/body/#app/uni-page-* 默认不显式 100% 高度,
   且 .room 仅靠 position:fixed inset:0 在某些模式下可能被祖先
   容器尺寸限制,导致 toolbar / tabBar 不贴底、内容挤在左上角
2. 刷新会议后再次加入会触发 toast "你不在当前会议中":
   cleanupStaleMeetings 对列表里已过期的会议调 leave 必然返回 400,
   被全局 request 拦截器误报给用户
3. 会议界面部分按钮点击无反应:
   根因同 #1,布局错位后按钮视觉位置与 hit box 不一致

修复
- App.vue: H5 条件编译注入全局基础样式,强制 html/body/#app/uni-app/
  uni-page/uni-page-wrapper/uni-page-body 宽高铺满,min-height 100%
- room.vue: .room 追加 width/height/min-width/min-height: 100vw/100vh 冗余
  + z-index:10,彻底兜底 position:fixed 失效场景
- request.js: 新增 options.silent 选项,业务错误 / 401 / 403 / 500 /
  网络异常均根据 silent 决定是否弹 toast;401 仍会跳登录
- api/meeting.js: createRoom / joinRoom / leaveRoom 均接受 options 参数
- store/meeting.js:
  · cleanupStaleMeetings 内 leave 传 silent:true 忽略幂等错误
  · joinAndEnter / createAndEnter 首次尝试(未重试时)传 silent:true,
    若命中 staleMeetingHint 自动清理重试,对用户完全无感;重试仍失败
    的真实错误走正常 toast

验证
- Playwright 1024x576:room 页 / profile 页 / chat 列表 / 刷新后重新加入
  流程截图均显示全屏铺满、tabBar/toolbar 贴底、无误报 toast
- 刷新 → join → 预览 → 加入 日志:首次 400 静默 → 自动清理 → 重试加入
  成功,MediaEngine 正常建立

Made-with: Cursor
2026-04-22 16:00:12 +08:00
bujinyuan
0c9f0483e5 fix(phase2e-2): 修复刷新后重入会失败 + 聊天输入区布局防御
问题
- 刷新会议页面被踢回 join 页,再次加入报"已在会议中"
- 极窄聊天面板下,发送按钮在 uni-app H5 渲染为 uni-button 时可能被默认
  width:100% 样式撑满导致"竖排变形"

修复
1. meeting store: joinAndEnter / createAndEnter 捕获 staleMeetingHint 时自动
   调用 cleanupStaleMeetings 清理后端残留的僵尸活跃成员记录,并重试一次;
   日志链路:加入失败 → 检测僵尸 → 自动清理 → 重试加入
2. ChatPanel: input-area 显式声明 flex-direction:row + flex-wrap:nowrap;
   .btn-send 追加 width:auto / line-height:1 / box-sizing,并通过 ::v-deep
   复位 uni-app 内部 <uni-button> 默认 width,彻底避免按钮独占一行变形

验证
- Playwright 刷新 room 页 → 回 join → 预览 → 加入会议:日志显示自动清理僵尸
  "581-702-753"、重试加入 "925-409-354" 成功,MediaEngine 正常建立
- 1024x576 视口下 toolbar 完整可见,聊天面板并排无右侧留白,发送按钮在
  输入框右侧

Made-with: Cursor
2026-04-22 15:45:56 +08:00
bujinyuan
16225c413d fix(phase2e-2): 会议内聊天面板 UI 布局优化
问题修复:
- 原遮罩式抽屉覆盖底部 toolbar,导致按钮不可点击
- 发送按钮过小不显眼
- 聊天面板打开时视频区被遮挡出现大片空白

改进:
- room.vue 主区改为 flex-row 布局:视频列 + 聊天列并排,聊天打开时视频自动收窄
- ChatPanel 去除全屏遮罩,作为侧边栏嵌入 chat-col,保留过渡动画
- 发送按钮增加纸飞机图标并采用渐变背景,hover / active / disabled 状态清晰
- msg-list 使用 flex:1 + min-height:0 + height:0 修复 scroll-view 高度撑破问题,保证输入区始终固定在底部
- 小屏(<750px)自动回退为全屏浮层,避免挤压视频

Made-with: Cursor
2026-04-22 15:32:10 +08:00
bujinyuan
1222bf551d feat(phase2e-2): 会议内聊天面板落地(Task 12)
- 新增 ChatPanel 组件:右侧抽屉式聊天 UI,支持发送/加载更早消息/未读清零
- store/meeting.js:扩展 chatHasMore/chatUnreadCount/chatHistoryLoaded 等状态与 loadMoreChats/markChatsRead 动作;WS 消息到达时按 id 去重
- room.vue:替换临时 toast 占位为 ChatPanel,绑定未读徽标、成员展示名映射;watch unread 自动在面板打开时清零
- 后端:SendChat WS 广播与 REST 响应补充 user_name/user_avatar,聊天面板无需额外查询即可展示昵称

Made-with: Cursor
2026-04-22 15:16:17 +08:00
bujinyuan
59f824ad27 feat(phase2e-2): 会议室主页与核心 UI 组件落地(Task 11)
新增页面:
- pages/meeting/room.vue:会议主容器,组装 VideoGrid/Toolbar/MemberPanel/InviteDialog
  * 顶部条:标题/会议号/复制、连接状态 pill、网络徽标、计时器
  * 主区:按成员数自适应网格渲染 VideoTile
  * 底部:麦/摄/邀请/成员/聊天/挂断工具条
  * 离会确认弹窗:主持人可选"结束会议 / 仅自己离开"
  * onUnload 兜底调用 leave 避免脏状态残留

新增组件(components/meeting/):
- VideoTile.vue:原生 video/audio 挂载(H5)+ 名字/静音/主持人/说话者边框
- VideoGrid.vue:1/2/3-4/5-9/10+ 响应式 CSS Grid 布局
- MeetingToolbar.vue:6 按钮工具条,loading 态 + 徽标
- MemberPanel.vue:右侧滑入抽屉,主持人可转让/踢出
- InviteDialog.vue:居中弹窗,一键复制会议号/链接/邀请文案
- NetworkBadge.vue:4 格信号柱,level 0-4 分色

配套改动:
- preview.vue:入会成功后跳 /pages/meeting/room?code=xxx(原先指向 debug)
- pages.json:注册 room 路由,navigationStyle=custom 隐藏默认导航
- store/meeting.js:remoteConsumers 外层 slot 解除 markRaw,使
  slot.audio/slot.video 赋值能驱动 VideoTile 响应式重渲染

验证:Playwright E2E 完成 create → preview → room 全链路
- 视频块、会议号、连接状态、计时器、工具条渲染正确
- 邀请弹窗、成员抽屉、静音切换、离会确认均工作正常

Made-with: Cursor
2026-04-22 14:41:27 +08:00
bujinyuan
eb3857a37a feat(phase2e-2): 前端创建/加入/设备预览 3 页落地(Task 10)
页面
- pages/meeting/create.vue:创建表单,标题(默认"{昵称}的会议"/40 字限制) + 密码(4-16 位)
  + 入会静音开关 + 允许聊天开关。提交时表单暂存到 store.draftCreatePayload,跳 preview 页
- pages/meeting/join.vue:会议号输入(9 位纯数字/带连字符均可,自动格式化 XXX-XXX-XXX)
  + 可选密码。支持 URL query code/password 回填(邀请链接一键入会入口)
- pages/meeting/preview.vue:设备预览页
  - navigator.mediaDevices.enumerateDevices() 列出摄像头/麦克风/扬声器
  - getUserMedia({video,audio}) 本地画面预览(用原生 <video> 避开 uni-input 对 srcObject 的限制)
  - AudioContext.AnalyserNode 以 rAF 采样 RMS,渲染渐变音量条
  - 显示名输入 + 入会默认开麦/开摄像头开关
  - 权限被拒/浏览器不支持 WebRTC 时给清晰错误提示
  - 离开页时释放 preview stream 与 AudioContext,避免占用摄像头

Store 扩展(meeting.js)
- 新增 state:draftCreatePayload(create→preview 暂存) + devicePreview(设备选择结果)
- createAndEnter(payload, mediaPrefs?) / joinAndEnter(code, password, mediaPrefs?) 新增
  可选 mediaPrefs 参数:{ startAudio, startVideo, audioDeviceId, videoDeviceId }
- _afterJoined 内按 mediaPrefs 入会后自动 startLocalAudio/Video(失败仅告警不阻断)
- startLocalAudio/Video 支持可选 deviceId,映射到 getUserMedia 的 deviceId.exact 约束

路由
- pages.json 注册 create/join/preview 3 条路由(均非 TabBar 页,从 URL 或后续入口跳入)

临时占位
- preview 页加入成功后暂时 redirectTo 到 debug.vue(Task 11 room 页待落地时改为 /pages/meeting/room)

Playwright 验证(真实 macOS 设备)
- create → preview:表单 "新昵称的会议" 正确携带到 preview 页
- preview 页:FaceTime 摄像头 + MacBook Pro 麦克风枚举成功,video readyState=4,音量条工作正常
- 点"加入会议":createAndEnter 成功,debug 页显示 localState=connected,主持人=是
  mediaPrefs 生效——麦克风/摄像头按钮显示"关闭麦克风/关闭摄像头"说明自动推流成功
- join 页:URL ?code=123-456-789&password=secret 正确预填,"下一步" 按钮启用

Made-with: Cursor
2026-04-22 14:20:57 +08:00
bujinyuan
467135e0d5 feat(phase2e-2): WS 稳定性改进——重连后自动重绑定房间 + 多端登录抖动检测
背景
- Task 9 联调发现同账号多 tab 登录会触发后端单设备踢下线机制,WS 每秒被关闭一次,
  所有 meeting.* 的 sendWithAck 都因 "连接已断开" 而失败,表现为"开启音视频提示失败"
- 同时 WS 断线重连后后端对该连接的 roomCode 绑定丢失,会议广播不再送达本端

改动
- store/meeting.js:_registerListeners 增加 WS 生命周期监听
  - _disconnected:从 CONNECTED/CONNECTING 切到 RECONNECTING,UI 可展示"重连中…"
  - _connected:在 RECONNECTING 状态自动重发 meeting.room.join 重绑定房间,并刷新参与者列表
- services/websocket.js:_recordCloseForFlapping 做 10s 滑窗 flapping 检测
  - 窗口内累计 >=3 次非主动断连 → 触发 _flapping 事件(含 count/windowMs)
  - 15s 冷却避免抖动期间重复告警
- debug.vue:新增 WS 状态指示灯(已连接/重连中/已断)+ flapping 警告横幅
  - 订阅 _connected / _disconnected / _flapping 三个 WS 内置事件,onBeforeUnmount 注销

验证(Playwright)
- 单次断连:testuser1 创建会议后手动 close WS → 指示灯 reconnecting,1s 后重连
  - 日志链 "WS 已断开 → 进入 reconnecting → 连接成功 → 重绑定房间 307-857-909 → 恢复 connected" 全通
- 4 次连续断连:第 3 次触发 flapping,UI 横幅 "⚠ 检测到异常重连(3 次/10s),可能同账号在其他端登录"
  - 每次断连后房号、主持人状态、localState 均正确恢复为 connected

Made-with: Cursor
2026-04-22 14:08:07 +08:00
bujinyuan
807e111cb9 chore(scripts): start/stop/status 接入 media-server(port 3300)
- start.sh:新增 start_media(),端口 3300,缺 .env 时自动从 .env.example 复制;
  缺 node_modules 时给出提示不跑命令;加入 all/独立子命令
- stop.sh:新增 stop_media(),通过 PID 文件 + 端口兜底停止媒体服务器
- status.sh:状态面板增加 "媒体服务器 SFU (:3300)" 行
- 完整用法:./scripts/start.sh [all|backend|frontend|admin|media|docker|--no-docker]

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