# Recording Transfer and Playback Runbook > 任务:S24 - 录音搬运与播放 > 完成时间:2026-06-21 20:20 +08:00 ## 1. 目标 S24 完成录音闭环的最小能力: - Server B 通过 Tailscale 私网从 Server A 拉取 `.ready` 录音文件。 - 拉取后校验文件大小和 SHA-256。 - 本地 `.part-*` 临时文件校验成功后原子改名。 - 校验和数据库标记成功后才允许删除 Server A 源 `.ready` 文件。 - API 提供受 RBAC 保护的播放入口,通过 Nginx `X-Accel-Redirect` 进入内部 `/_recordings/` location,支持 Range。 ## 2. 代码产物 | 文件 | 说明 | | --- | --- | | `apps/worker-recording/src/transfer.ts` | 录音扫描、SSH 拉取、大小校验、SHA-256、原子落盘、源文件安全删除、`recordings` 入库 | | `apps/worker-recording/src/main.ts` | Recording Worker 主循环 | | `apps/worker-recording/src/transfer.spec.ts` | 搬运、哈希、删除保护和路径安全测试 | | `apps/api/src/modules/recordings/*` | `GET /api/v2/recordings/:id/play` 播放入口 | | `apps/api/src/modules/recordings/recordings.service.spec.ts` | Nginx 内部路径和路径穿越防护测试 | | `infra/server-a/s19/scripts/lisglosips-recording-finalize` | A 端 finalizer 仓库副本,ready 目录改为 `0770`,便于专用拉取用户校验后删除源文件 | ## 3. Worker 环境变量 ```text DATABASE_URL RECORDING_REMOTE_HOST=lisglosips-a RECORDING_SSH_CONFIG=/etc/lisglosips/recording/ssh_config RECORDING_REMOTE_READY_DIR=/dev/shm/voip_rec/ready RECORDING_LOCAL_ROOT=/data/recordings RECORDING_SCAN_INTERVAL_MS=10000 RECORDING_MAX_FILES_PER_SCAN=50 RECORDING_DELETE_SOURCE_AFTER_COPY=true ``` 本地开发可使用 `.codex-private/ssh/config`;生产或 B 端 systemd 应使用 `/etc/lisglosips/recording/` 下的受控 key 和 known_hosts。 ## 4. 服务器变更 ### Server B - 新增 `/etc/lisglosips/recording/a_pull_ed25519`,权限 `0640 root:lisglosips`。 - 新增 `/etc/lisglosips/recording/known_hosts`。 - 录音持久化目录仍为 `/data/recordings`,由 `lisglo-recorder:lisglosips` 管理。 - 备份目录:`/var/backups/lisglosips-s24/20260621T121100Z`。 ### Server A - 新增专用用户 `lisglo-rec-pull`,加入 `rtpengine` 组。 - 安装 B 端录音拉取公钥到 `/var/lib/lisglo-rec-pull/.ssh/authorized_keys`。 - 将 `/dev/shm/voip_rec/ready` 调整为 `0770 rtpengine:rtpengine`。 - 更新 `/usr/local/sbin/lisglosips-recording-finalize`,让 ready 根目录和 ready 子目录保持 `0770`。 - 备份目录:`/var/backups/lisglosips-s24/20260621T121130Z`。 未调整 SSH 管理端口,未禁止 SSH key 登录,未重启 OpenSIPS、RTPEngine、Nginx 或 API。 ## 5. 播放路径 API: ```text GET /api/v2/recordings/:id/play Permission: recordings.play ``` API 只查询状态为 `READY` 的录音,返回: ```text X-Accel-Redirect: /_recordings/ Content-Type: audio/wav 或 audio/mpeg Cache-Control: private, no-store ``` Nginx 已在 S05 配置: ```text location /_recordings/ { internal; alias /data/recordings/; add_header Accept-Ranges bytes always; } ``` 外部直接访问 `/_recordings/...` 返回 `404`,只能经 API 鉴权后内部转发。 ## 6. 验证结果 本地: - `corepack pnpm@10.33.0 exec vitest run apps/worker-recording/src/transfer.spec.ts apps/api/src/modules/recordings/recordings.service.spec.ts` 通过,5 条测试。 - `corepack pnpm@10.33.0 --filter @lisglosips/worker-recording build` 通过。 - `corepack pnpm@10.33.0 typecheck` 通过。 - `corepack pnpm@10.33.0 lint` 通过。 - `corepack pnpm@10.33.0 build` 通过。 服务器冒烟: - A 生成测试文件 `/dev/shm/voip_rec/incoming/s24-smoke/s24-smoke.wav`。 - A finalizer 生成 `/dev/shm/voip_rec/ready/s24-smoke/s24-smoke.wav.ready`。 - B 使用 `lisglo-recorder` 和专用 SSH key 从 A 私网拉取。 - B 落盘 `/data/recordings/s24-smoke/s24-smoke.wav`,大小 `31`,属主 `lisglo-recorder:lisglosips`。 - A/B SHA-256 一致:`c72dd3909a317ad55817435b7ed5a0db6947455ffc66e09a2dde787fe4a8c348`。 - 校验成功后 A 源 `.ready` 文件已删除。 - 外部直接访问 `https://127.0.0.1/_recordings/s24-smoke/s24-smoke.wav` 返回 `404`,内部录音 location 未暴露。 ## 7. 当前限制 B 当前 `/opt/lisglosips/current` 仍是 S05 placeholder release,未切换到完整 monorepo API/Worker release。因此 S24 本次完成代码、构建、服务器私网拉取链路和 Nginx 内部播放保护验证;正式 `lisglosips@recording-worker.service` 随完整应用 release 发布时启用。 ## 8. 回滚 Server A: ```bash sudo cp -a /var/backups/lisglosips-s24/20260621T121130Z/lisglosips-recording-finalize /usr/local/sbin/lisglosips-recording-finalize sudo chmod 0755 /usr/local/sbin/lisglosips-recording-finalize sudo userdel -r lisglo-rec-pull sudo chmod 0750 /dev/shm/voip_rec/ready ``` Server B: ```bash sudo rm -rf /etc/lisglosips/recording sudo rm -rf /data/recordings/s24-smoke ``` 代码回滚: - 恢复 `apps/worker-recording/src/main.ts` 到骨架版本。 - 删除或恢复 `apps/worker-recording/src/transfer.ts`、`transfer.spec.ts`。 - 删除 `apps/api/src/modules/recordings/` 并从 `AppModule` 移除 `RecordingsModule`。 - 恢复 `apps/worker-recording/package.json`、`tsconfig.json`、`pnpm-lock.yaml`。 - 重新执行 `corepack pnpm@10.33.0 build`。