# Recording Transfer and Playback Runbook > 任务:S24 - 录音搬运与播放 > 完成时间:2026-06-21 20:20 +08:00 > 当前模式:S56 - Server B 本机录音闭环(2026-08-27) ## 0. 当前生产架构(S56) 录音不再经过 B 到 A 的专用 SSH。Server B 上的 RTPEngine Recording Daemon 先写入 `/dev/shm/voip_rec/incoming`,本机 finalizer 将稳定文件原子改名为 `/dev/shm/voip_rec/ready/**/*.ready`,Recording Worker 再复制、校验并持久化到 `/data/recordings`,最后写入数据库并删除本机 `.ready` 源文件。 ```text B RTPEngine -> B incoming -> B finalizer -> B ready -> B Recording Worker -> B /data/recordings + MySQL ``` 旧的 B -> A SSH 配置仅作为短期回滚材料保留,不参与当前服务。S24 以下章节保留为历史实施记录。 ## 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 环境变量 S56 当前生产配置: ```text DATABASE_URL RECORDING_SOURCE_MODE=local RECORDING_LOCAL_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 ``` `RECORDING_SOURCE_MODE=ssh` 仍受代码支持,以下变量只用于回滚到旧架构: ```text DATABASE_URL RECORDING_SOURCE_MODE=ssh 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 ``` SSH 模式本地开发可使用 `.codex-private/ssh/config`;生产回滚时才使用 `/etc/lisglosips/recording/` 下的受控 key 和 known_hosts。 ## 4. 服务器变更 ### Server B - S56 新增 `lisglosips-recording-finalize.timer`,每 5 秒检查本机稳定录音文件。 - Recording Worker 以 `lisglo-recorder` 身份运行,通过 `SupplementaryGroups=rtpengine` 访问 ready 目录。 - systemd 只开放 `/dev/shm/voip_rec/ready` 与 `/data/recordings` 所需写权限。 - Worker 使用 `RECORDING_SOURCE_MODE=local`,运行期不再启动 `ssh` 子进程。 - 新增 `/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. 当前限制 S24 记载的 placeholder 限制已经失效;B 已运行完整 monorepo API/Worker release。S56 不迁移 A 的历史录音,也不自动删除旧 SSH key;旧凭据待本地闭环稳定运行并完成真实录音验收后再单独清理。 ## 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 systemctl disable --now lisglosips-recording-finalize.timer sudo cp /var/backups/lisglosips-s56//recording-worker.env /etc/lisglosips/recording-worker.env sudo cp /var/backups/lisglosips-s56//10-s28-recording.conf /etc/systemd/system/lisglosips@recording-worker.service.d/10-s28-recording.conf sudo systemctl daemon-reload sudo systemctl restart lisglosips@recording-worker ``` 代码回滚: - 恢复 `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`。