Files
EchoChat/docs/plans/2026-04-21-phase2e-2-implementation.plan.md
bujinyuan 55d6352c6a feat(phase2e-2): 落地 HTTPMediaOrchestrator 打通 Go↔Node 媒体链路(Task 7)
- 新增 MediaServerConfig + config.{dev,docker}.yaml 的 media_server 段
  (base_url / internal_token / timeout_ms / close_timeout_ms / close_retry)
- 新建 HTTPMediaOrchestrator(8 方法 + sync.Map 缓存 roomCode→routerID
  + 关闭类指数退避重试 200/500ms + ErrMediaResourceNotFound/ErrMediaServerError
  两类错误)实现 MediaOrchestrator 接口
- wire 绑定由 NoopMediaOrchestrator 切换到 HTTPMediaOrchestrator;
  MeetingService / MeetingSignalService 调用侧零改动
- E2E 脚本 docs/verify/meeting_t7_verify.mjs 证明 16/16 PASS:
  健康检查/错token 401/REST 创房加入/WS room.join/真实 mediasoup
  transport.create(ICE/DTLS 指纹均由 Node 返回非占位)/404 幂等关 producer
  /host 结束会议触发 CloseRouter
- 同步更新 13 份文档:CURRENT_STATUS / implementation.plan / design
  变更记录 / project-context / api 导览 / api websocket / api frontend meeting
  / system-architecture / media-server README / 顶层 README 等
- go build ./... 全绿

Made-with: Cursor
2026-04-21 17:47:11 +08:00

615 lines
43 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.

# Phase 2e-2 实施计划:会议 MVP多人音视频
> **状态:** 📋 待执行(设计文档定稿后进入代码开发)
> **设计文档:** [Phase 2e-2 设计文档](./2026-04-21-phase2e-2-design.md)
> **上级路线图:** [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)
> **分支:** `feature/phase2e-2-meeting-mvp`
> **预估总工时:** **约 17 人日**17 个 Task含 PoC 与 UI 打磨)
> **最后更新:** 2026-04-21Task 0-7 ✅ 已落地Go↔Node HTTP 媒体链路打通,下一步 Task 8 生命周期状态机)
---
## 一、范围锁定
### 本期交付(与设计文档 §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 依赖拓扑
```mermaid
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.ts`
- `src/mediasoup/{worker.ts, router.ts, codecs.ts}`
- `src/routes/{router,transport,producer,consumer}.route.ts`
- `src/services/{router,transport,producer,consumer}.service.ts`
- `src/middlewares/internal-auth.ts`
- `src/utils/logger.ts`
- `tests/*.spec.ts`
- `package.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 0mediasoup PoC Spike ✅
- **状态**:✅ 已完成2026-04-21
- **目标**:在正式动工前用最小代码跑通"2 个浏览器 ↔ Node ↔ mediasoup ↔ 互相看见视频",验证技术栈可用性与关键参数
- **依赖**:无(起点)
- **实际产出**
- [`media-server/poc/`](../../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`](../../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 RSSNode 单进程毫无压力)
- peer 关闭后资源自动清理consumers 从 4 → 0无泄漏
- **已锁定的关键坑**
- 本机 Demo `MEDIASOUP_ANNOUNCED_IP` 必须留空(非 `127.0.0.1`),让 Chromium 自动替换 `0.0.0.0` 为可用地址
- mediasoup-client 无 UMD bundlePoC 走 `esm.sh` CDNTask 9 正式前端改用 `npm + vite`
- Consumer 必须以 `paused:true` 创建,客户端 `transport.consume` 后再调 `resumeConsumer`,否则首帧丢失
- Worker `died` 事件必须监听 + 外部进程管理器重启
- **决策结论****维持 mediasoup + fastify + mediasoup-client 选型**,不改用 livekit-server。技术栈可用性已满足 MVP 需求。
- **工作量**1 人日(实际用时吻合估算)
### Task 1media-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 / vitest
- `media-server/tsconfig.json` + `tsconfig.build.json` — TS 5.5 严格模式、ES2022 + Bundler 解析
- `media-server/.eslintrc.json` + `.prettierrc`@typescript-eslint + prettier 协同,强制 `consistent-type-imports`
- `media-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-prettydev 可选)+ token redact + `childLogger`
- `media-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-slimbuilder 装 python3 + build-essential 编译 mediasoup workerruntime 裁 dev deps + 非 root 用户 + curl HEALTHCHECK + 显式暴露 `40000-40199/UDP+TCP`
- `media-server/README.md` — 目录结构 / 快速开始 / 鉴权校验 / Docker 构建 / 配置说明 / Task 1 验收清单
- **实测验收**(已通过):
- `npm install` 成功315 包mediasoup C++ worker 编译通过)
- `npm run typecheck` 0 错误、`npm run lint` 0 错误
- `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 后自动拉起新 WorkerPID 变更),日志出现 "worker died" → "worker started"
- **关键决策**
1. **Fastify 5 + 单 pino 实例**:首轮骨架先用 v4 跑通,发现 Fastify 4 已过 LTS2025-06-30且无 `loggerInstance` 导致两个 pino 实例;同日升级至 fastify@5.8.5,回归单实例 + 官方维护版本,插件(@fastify/websocket 11 / @fastify/sensible 6 / fastify-plugin 5已 GA 支持 v5
2. mediasoup 3.19 类型从 `mediasoup/types` 子路径引入(`Worker` / `WorkerLogLevel`
3. 内部鉴权使用 `timingSafeEqual` 替代 `===`,对长度不等先拉齐再比,防 token 长度时序探测
4. Worker 指数退避 `1s → 2s → 4s → 8s → 16s → 30s`(封顶),重启成功后 `restartAttempts` 归零
5. Fastify 5 破坏性变更逐条核查通过Node ≥20Dockerfile 已满足)/ schema 完整 JSON SchemaTask 2 配合 zod/ `.listen()` 对象签名 / plugin 纯 async
- **工作量****0.5 人日**(实际用时吻合估算,含 Fastify 4→5 升级 30 分钟)
### Task 2Node 侧 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_ERROR` 5 种 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` / `lint` 0 错误
-`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 /transportssend+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 / Nitsm4/m6~m10、n1~n10登记至 `CURRENT_STATUS.md` Task 16 收尾清单
- **工作量****2 人日**(实际用时吻合估算;审查修复耗时 ~0.3 人日,已含在内)
### Task 3数据库 DDL + 模型 + DAO ✅
- **目标**PostgreSQL 3 张表落地 + Go 侧 model/dao 完整实现
- **依赖**T0
- **主要产出**
- `deploy/docker/postgres/init.sql` 追加 3 张表 + 9 索引 + COMMENT对齐设计 §5.1
- `deploy/docker/postgres/phase2e2_migration.sql` 增量升级脚本(`IF NOT EXISTS` 幂等,用于已运行环境无损追加)
- `backend/go-service/app/meeting/model/{meeting_room,meeting_participant,meeting_chat}.go`3 个 model + GORM 复合索引 tag + `IsActive()` 等辅助方法)
- `backend/go-service/app/meeting/dao/*.go`24 个持久化方法room 9 + participant 11 + chat 4`JoinRoom` 重入复用事务、`TransferHost` 角色交接事务、`LeaveRoom` 基于 `EXTRACT(EPOCH ...)` 的 DB 端 duration 计算、`FindActiveByUser` 单点参会校验、`MarkEnded` 乐观锁、`ListExpiredForCleanup` 批量扫描)
- `backend/go-service/app/constants/meeting.go`(单文件承载会议类型/状态/角色/结束原因/离会原因/默认配置/WS 事件 8 组常量 + `*Map` 中文映射)
- **检查点**
- `docker exec postgres psql < phase2e2_migration.sql` 幂等应用3 表 + 9 索引 + 外键全部正确 ✅
- psql 集成脚本跑通 8 场景CRUD、`room_code` UNIQUE、`(room_id,user_id)` UNIQUE、主持人转让事务`role=1→0` + `role=0→1` + `host_id` 更新)、聊天写入、`duration=10s` 精确匹配、CASCADE 删除 room 后 participants/chats 残留 0 ✅
- `go build ./...` / `go vet ./...` / `ReadLints` 零错误 ✅
- **实际产出 vs 计划差异**(关键风格修正):
1. **常量目录**:草案写的 `app/meeting/constants/{meeting_status,meeting_role}.go` 与项目实际风格不符;按 `project-context.mdc` 第 11 条「代码风格全局一致(最高优先级)」,采用 `app/constants/meeting.go` 单文件承载所有会议常量(与 `group.go`/`notify.go` 同构)。
2. **时间字段**:草案写 `TIMESTAMPTZ`,项目所有表统一 `TIMESTAMP(0)`(见 init.sql本次改为 `TIMESTAMP(0)` 对齐Go model 配 `gorm:"type:timestamp(0)"`
3. **冗余索引移除**:草案写 `idx_meeting_rooms_code`,但 `room_code UNIQUE` 已自动建 B-tree冗余索引已移除。
4. **Go 单元测试**:项目 Go 侧 0 个 `_test.go`(沿用"代码审查 + Playwright E2E"验证模式),本次同样不新增 `_test.go`;改用 psql 真库集成脚本覆盖 DAO 核心路径,验证价值等价且避免破坏项目一致性。
- **工作量****0.5 人日(实际 0.5,吻合估算)**
### Task 4Go 侧 meeting 模块骨架 + Wire ✅2026-04-21 完成)
- **目标**controller/service/router/provider 空壳搭建 + Wire 绑定完成
- **依赖**T3
- **实际产出**
- `backend/go-service/app/meeting/service/interfaces.go`25 行):`NotifyPusher` / `UserInfoResolver` / `OnlineChecker` 三接口;`OnlineChecker.IsOnline` 对齐 `ws.OnlineService` 实际签名(单 `bool` 返回)
- `backend/go-service/app/meeting/service/meeting_service.go`165 行):`MeetingService` + 8 个 sentinel error + 17 个空方法占位(返回 `ErrNotImplemented`
- `backend/go-service/app/meeting/controller/meeting_controller.go`150 行12 个 Gin handler + `responseNotImplemented`501+ `requireUserID` 辅助
- `backend/go-service/app/meeting/router.go`35 行,**扁平化:没有建 `router/` 子目录**12 条路由挂 `/api/v1/meeting/*` 并套 `jwtAuth`
- `backend/go-service/app/meeting/provider.go`22 行):`MeetingSet = wire.NewSet(DAO×3, Service, Controller)`
- `backend/go-service/app/provider/wire.go`(改 +10 行):挂入 `MeetingSet` + 3 条 `wire.Bind`
- `backend/go-service/app/provider/provider.go`(改 +6 行):`App``MeetingService/MeetingController` 字段
- `backend/go-service/app/provider/wire_gen.go`(自动重生成 +30 行)
- `backend/go-service/router/router.go`(改 +3 行):`meetingApp.RegisterRoutes(engine, app.MeetingController, jwtAuth)`
- `backend/go-service/app/admin/provider.go`(改 +6 行):**存量修复** 补齐 `MessageManage{DAO,Service,Controller}` provider
- **实际检查点**
- `go build ./...` / `go vet ./...` / `wire ./app/provider` 全部零错误
- `GIN_MODE=debug go run cmd/server/main.go` 启动无报错,`HTTP 服务启动` 日志出现在 `:8085`
- gin 启动日志打印全部 12 条 `[GIN-debug] ... meeting/controller.(*MeetingController).XxxRoom-fm (6 handlers)`
- 无 token curl `POST /api/v1/meeting/rooms` / `GET /api/v1/meeting/rooms` / `POST /api/v1/meeting/invites/:token/redeem` → 全部 401 `缺少认证信息`JWT 中间件生效
- **实际产出 vs 计划差异**
- **路由目录扁平化**:计划写 `app/meeting/router/meeting_router.go`,实际为 `app/meeting/router.go`,与项目内 `app/group/router.go` / `app/notify/router.go` 命名一致;`RegisterRoutes(engine, controller, jwtAuth)` 签名保持
- **provider 目录扁平化**:计划写 `app/meeting/provider/provider.go`,实际为 `app/meeting/provider.go`,与其他模块一致
- **新增 `OnlineChecker` 接口**(计划未列):未来业务逻辑需要判断被邀请者在线状态进行推送路由选择,提前抽象出来
- **顺手修复 admin wire 存量 bug**计划未列Task 4 重生成 wire 时暴露了 admin 模块 `MessageManage` 系列 provider 缺失的遗留问题,当场补上避免阻塞后续开发
- **工作量****0.5 人日**(实际约 0.4 人日,含存量问题修复约 0.1 人日)
### Task 5会议 REST 接口(创建/加入/离开/结束/列表/详情 + 邀请链接兑换 + 邀请2026-04-21 完成)
- **目标**:填充 Task 4 骨架中的业务逻辑,完整实现设计 §6.2 的 12 个接口
- **依赖**T4
- **主要产出**(全部实装并端到端通过验证):
- **DTO 层**`backend/go-service/app/dto/meeting_dto.go`169 行)定义 13 个 DTO`MeetingRoomDTO`/`MeetingParticipantDTO`/`MeetingChatDTO` 基础 + 10 个请求/响应类型),所有请求体有 `binding` 标签
- **工具层**`backend/go-service/pkg/utils/meeting_code.go` 会议号生成(`crypto/rand` + 3 组 3 位数字生成 `XXX-XXX-XXX`,冲突重试 5 次)+ 邀请 Token32 位 hex
- **Service 层**`MeetingService` 12 业务方法 + 11 个 sentinel 错误 + 4 个辅助函数(`assertIsActiveParticipant`/`assertIsHost`/`generateUniqueRoomCode`/`broadcastToActiveParticipants`
- **Controller 层**12 个 Gin 处理器 + `handleError` 领域错误 → HTTP 映射404/403/400/500 四档)+ DTO 转换辅助(`roomToDTO`/`participantToDTO`/`chatToDTO`
- **Stub 接口**:新增 `MediaOrchestrator` 接口 + `NoopMediaOrchestrator`Task 7 替换WS 广播走 `pubsub.PublishToUser` 逐人Task 6 改为 `BroadcastToMeeting``NotifyPusher.PushBatch` 复用 Phase 2e-1
- **路径修正**`router.go` 将 Task 4 占位路径对齐设计:`GET /rooms``GET /rooms/mine``POST /invites/:token/redeem``POST /invite-tokens/:token/redeem`
- **DAO 契约修复**`meeting_room_dao.GetByID/GetByCode` + `meeting_participant_dao.GetByRoomAndUser/FindActiveByUser``gorm.ErrRecordNotFound` 转为 `(nil, nil)`service 统一 `result == nil` 判定
- **密码限流**:同 `(user_id, code)` 5 次错误 → Redis `echo:meeting:pwd:fail:...` 锁 10 分钟(`ErrMeetingPasswordLocked`
- **单点参会**:用 `meeting_participants` JOIN `status != 2` 判断用户是否已在其他活跃会议(`ErrAlreadyInOtherMeeting`
- **host 自动转让**host 离会时若仍有其他活跃成员 → 自动将 host 转给"最早加入者",广播 `meeting.host.changed`;若无人则房间 `ended_reason=empty_ttl`
- **邀请 Token 安全**:响应不返回 token仅通过 `NotifyPusher.PushBatch.Extra.invite_token` 定向下发;兑换后保留 60 秒冗余由 Redis TTL 自然过期
- **API 文档**`docs/api/frontend/meeting.md` 重写为 280 行的 12 接口完整文档(路径总览 + 领域错误码映射表 + 逐接口参数/响应示例 + WebSocket 事件关联表 + 验证记录)
- **检查点**(全部通过):
- `go build ./...` / `go vet ./...` / `wire ./app/provider` 零告警
- 端到端脚本 `/tmp/meeting_t5_test.sh` 用 3 用户场景覆盖12 接口 happy path + 5 类错误路径(密码错 / 房间不存在 / 单点参会冲突 / 非 host 越权 / 邀请链接失效)→ **PASS=19 / FAIL=0**
- DB 侧核验 `meeting_rooms.status` / `meeting_participants.left_at/duration` / `meeting_chats` 写入正确Redis 侧核验 `echo:meeting:invite:{token}` TTL=600s
- 服务日志全链路 trace_idWS 广播 `meeting.member.joined/left/chat/host.changed/room.ended` 事件全部发出
- **实际产出 vs 计划差异**
- **密码连续错误 5 次锁 10 分钟**Task 5 已实现,与计划一致
- **容量限制**MVP 硬上限为 **8**(设计 D05超过将 `ErrMeetingFull`;计划里误写"第 9 人加入返回 meeting_full"表述已与硬上限对齐
- **`kick` 请求体**:设计文档曾讨论 `{target_user_id, request_id}` 的幂等字段Task 5 DTO 定义为 `{user_id}`(与 `TransferHostRequest.target_user_id` 命名区分),`request_id` 幂等保护留待 Task 6 WS 侧统一处理WS 场景更多)
- **InviteUsersResponse**:出于安全考虑不返回 token仅返回 `{pushed, skipped}`;测试时通过 Redis 获取 token
- **错误码中文化**:使用中文 `message`(与项目惯例一致)而非英文 `meeting_not_found` code前端通过 HTTP 状态码 + trace_id 区分
- **工作量****实际 1 人日**< 预估 1.5 人日 DTO 设计充分 + DAO 契约修复一次到位
### Task 6WS 信令 13 事件处理器 ✅ **已完成2026-04-21**
- **目标**实现设计 §6.3 WS 事件族最终落地 13 = 3 房间 + 5 成员 + 5 媒体完整对接到 `ws.Hub`
- **依赖**T4
- **实际产出**
- `app/constants/meeting.go`WS 事件常量与设计 §6.3 对齐 + `MeetingWSClientEvents` 白名单切片8 CS 事件
- `app/meeting/service/interfaces.go`重构`MediaOrchestrator` 扩容至 9 方法 + 5 DTO + `NoopMediaOrchestrator` 9 占位实现
- `app/meeting/service/meeting_broadcaster.go`新建75 统一广播层 `BroadcastToMeeting` + `PublishToUser`REST / WS 共用
- `app/meeting/service/meeting_service.go`重构12 REST 方法改调 `broadcaster.*`不再直连 `ws.PubSub`
- `app/meeting/service/meeting_signal_service.go`新建430 8 CS 事件业务 + Redis 资源追踪 + 资源清理 + host 权限校验
- `app/meeting/controller/meeting_ws_handler.go`新建200 薄层 controller`hub.RegisterEvent` 注册 + JSON 反序列化 + ACK
- `app/meeting/provider.go` + `app/provider/provider.go`Wire 挂入新 3 provider
- `docs/api/frontend/meeting.md`追加 2 +200 ):§WebSocket 信令协议Task 6 + 验证记录
- **实际检查点**
- `/tmp/meeting_ws_t6_test.mjs` 端到端 **18/18 PASS**8 CS 白名单事件 + 3 SC 广播 + 3 类错误路径 host 越权/不存在会议号/WS leave 资源清理
- host 尝试 `meeting.member.state.changed` 修改他人 ACK `code=-1 message=仅主持人可执行此操作`
- `meeting.room.leave` 自动触发 `mediaOrchestrator.Close*` 清理 Redis 资源 Set
- `go build` / `go vet` / `wire` 全绿
- **偏离与说明**
- 最终事件数从实施计划的 11 个扩展为 13 + 2 个业务补充事件`meeting.chat.message` + `meeting.member.producer.new`与设计文档 §6.3 一致
- 权限校验入口改为在 `MeetingSignalService.On*` 方法内部调用 `assertIsActiveParticipant` / `assertIsHost`不再依赖 handler 层前置断言代码更易测试
- 广播 API 统一为 `MeetingBroadcaster.BroadcastToMeeting` `excludeUserIDs ...int64` 可变参数比计划中"`Hub.BroadcastToMeeting` 方法" 更内聚不污染 `ws.Hub` 通用接口
- **实际工作量****1 人日**比预估 1.5 人日节省得益于 Task 5 已预置好 DAO / 错误链 / DTO
### Task 7Go → Node HTTP Client 封装 ✅ 已完成2026-04-21
- **目标**实现设计 §6.6 `NodeClient` 接口挂接到 WS 信令流程
- **依赖**T2 + T6
- **实际产出**
- `app/meeting/service/http_media_orchestrator.go`340 实现 `MediaOrchestrator` 8 方法完整覆盖 Router/Transport/Producer/Consumer 生命周期
- `config/config.go` + `config.dev.yaml` + `config.docker.yaml`新增 `MediaServerConfig{BaseURL, InternalToken, TimeoutMS, CloseTimeoutMS, CloseRetry}`支持环境变量 `ECHOCHAT_MEDIA_SERVER_*` 覆盖
- `app/meeting/provider.go` + `app/provider/wire_gen.go``wire.Bind` `*NoopMediaOrchestrator` 切换为 `*HTTPMediaOrchestrator`注入 `*config.Config`
- `docs/verify/meeting_t7_verify.mjs`140 Node.js E2E 验证脚本
- 错误类型`ErrMediaResourceNotFound`404关闭类自动幂等转 nil/ `ErrMediaServerError`5xx/超时/网络错可供上层 `errors.Is` 区分
- 关闭类指数退避200ms 500ms最多 `CloseRetry + 1` 次尝试
- `sync.Map` 本地 `roomCode ↔ routerID` 缓存兼容设计 §6.6 "CloseRouter(roomCode)" 签名约束
- **检查点已全部达成**
- `go build ./...` 全绿
- `go vet ./...` 全绿
- 端到端脚本 `docs/verify/meeting_t7_verify.mjs` **16/16 PASS**覆盖健康检查token 鉴权REST 创建/加入/结束会议WS transport.create404 幂等关闭
- media-server 日志确认真实 `router created` / `webrtc transport created` / `router closed explicitly`
- `transport.id` / `iceCandidates[]` / `dtlsParameters.fingerprints[]` 均非占位证明真实 mediasoup 链路
- **偏离与说明**
- 文件命名由设计 `node_client.go` 改为 `http_media_orchestrator.go` Task 6 抽出的 `MediaOrchestrator` 接口保持语义一致
- 接口方法数保持 8 设计 §6.6 9 个含 `ResumeConsumer``ResumeConsumer` Node REST 已就绪但当前 WS 契约无对应事件留给 Task 9 前端 mediasoup-client 接入时补齐
- 单元测试`httptest.NewServer` 模拟本次未落地用更真实的"Go + Node + Playwright-style E2E"替代证据力更强Task 10可观测性补齐单测
- **实际工作量****0.5 人日**符合预估
### Task 8会议生命周期状态机host 宽限期 + 自动转让 + 空房 TTL
- **目标**实现设计 §6.5 的状态机完整逻辑
- **依赖**T5
- **主要产出**
- `app/meeting/service/meeting_service.go` 补充
- `OnHostDisconnect(ctx, roomCode)` `echo:meeting:host_grace:{code}` EX 120s
- `OnHostReconnect(ctx, roomCode, userID)` 清除宽限期键
- `HandleHostGraceExpired(ctx, roomCode)` 转让主持挑最早加入者或销毁会议
- `OnAllMembersLeft(ctx, roomCode)` 设置房间 Redis key TTL 300s
- `OnRoomTTLExpired(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.Device`
- `createSendTransport` / `createRecvTransport` 包装 + 事件桥接到 WS
- `produceAudio(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}.vue` 3 个页面
- **依赖**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 网格切换 1234 人布局正常
- 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 14docker-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.md`
- `design-system/echochat/pages/meeting-preview.md`
- `design-system/echochat/pages/meeting-room.md`
- `design-system/echochat/pages/meeting-invite.md`
- 代码落地
- `VideoTile.vue` 说话者流光轮廓动效
- 柔性网格2/3 人布局的非等分样式
- 自视频浮窗四角吸附`draggable` + 吸附计算
- 静音氛围色工具栏底色绑定 computed `allMuted`
- `NetworkBadge.vue` 3 档波浪动效
- **检查点**
- 视觉走查4 屏与设计产物一致色彩 / 留白 / 动效符合飞书简洁 + EchoChat 原创要求
- 动效性能不影响 60fpsChrome Performance 面板确认
- 代码审查`ui-ux-pro-max` 产物被真正使用不留下"代码 vs 设计产物"脱节
- **工作量****2 人日**
### Task 16E2E 验证 + 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` 落盘
- **检查点**
- 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 120.5 人日
- 纳入了**移动端响应式布局**分摊到 Task 11/15 1 人日
- 纳入了**邀请通知对接** Phase 2e-1 的闭环Task 13
- 纳入了**`ui-ux-pro-max` 定制 UI 打磨**Task 152 人日
- 预留了 **PoC Spike 与 E2E** 的显式工时Task 0 / Task 16 2 人日
该增量均是用户在 Plan 模式下的 11 项决策明确要求纳入的已与设计文档 §2.2 对齐
---
## 六、工作流与提交节奏
1. **分支**`feature/phase2e-2-meeting-mvp`已由上游决策指定
2. **每个 Task 完成后**
- 本地 `go test ./app/meeting/...` / `pnpm test` / `npm run lint` 全绿
- `code-reviewer` 子代理小审仅重要 Task 5/7/9/11 跑全量审
- 小粒度 commit`feat(meeting): task-N <简短描述>`
- ****自动推远端由用户显式触发
3. **每周节奏**Task 0-4 集中第一周基础设施 + 骨架Task 5-13 分散第二周业务主体Task 14-16 第三周部署 + 打磨 + 收官
4. **阻塞处理**
- PoCTask 0如失败立即暂停与用户重议选型livekit-server 替换 mediasoup
- 任何 Task 工时超出估算 50% 以上立刻停下来复盘必要时拆子任务或推迟非关键部分
5. **Review Gate**每完成 3-4 Task 触发一次 review gate 汇报阶段进展
---
## 七、显式不做(本实施计划范围外)
- 不写 Phase 2e-3 相关代码预约 / 提醒 / 等候室 / 锁定
- 不写管理端 meeting 模块代码推迟到 Phase 2f
- 不做录制 / 屏幕共享 / 虚拟背景二期
- 不做多 Worker / Node 实例 / Router Pipe三期
- 不引入 CI 流水线接入后再由运维任务统一实施
- 不做付费 TURN 服务接入MVP 自建 coturn 即可
---
## 八、关联文档
- [Phase 2e-2 设计文档](./2026-04-21-phase2e-2-design.md)
- [Phase 2e 整体路线图](./2026-04-20-phase2e-design.md)
- [Phase 2e-1 实施计划](./2026-04-20-phase2e-1-implementation.plan.md)参考格式
- [系统总设计](./2026-02-27-echochat-system-design.md)
- [项目规则](../../.cursor/rules/project-context.mdc)
- [API 文档索引](../api/README.md)
---
## 九、变更记录
| 日期 | 作者 | 变更 |
|---|---|---|
| 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.15.8.5fastify-plugin@45.1.0@fastify/sensible@56.0.4@fastify/websocket@1011.2.0`src/app.ts` 改用 `loggerInstance: logger` 消灭 pino 双实例理由Fastify 4 已过 2025-06-30 LTS 支持v5 生态 GA单实例回归回归测试全部通过 |
---
**文档结束**