Files
story/docs/media-upload-and-access.md
T

85 lines
10 KiB
Markdown
Raw Normal View History

2026-08-26 06:41:07 +08:00
# 双轨录音上传、校验与访问开发说明
## 1. 目标与边界
本模块实现长者声音`near`和智能体声音`far`两条录音轨道的分片声明、私有对象直传、可靠校验、播放/下载独立授权和访问审计。实时语音仍由终端、RTC和语音智能体直接传输;录音字节不经过业务HTTP接口,也不写入应用服务器系统盘。业务API只处理元数据和短期单对象地址,Worker从私有对象存储流式核验文件。
终端在通话心跳或离线批量遥测中上报`recording_queue.pending_segments`、`failed_segments`以及可选的脱敏`last_failure_code/last_failure_at`。家庭端设备卡片从设备影子选择时间最新的队列报告,显示待上传数量、失败数量和最近失败原因;失败码缺失时明确提示终端尚未上报,不把未知原因伪装成成功。云端完成对象完整性与WAV校验前,页面不会提示终端删除本地密文。
录音上传同时受两层授权约束:创建通话时冻结的授权必须允许`transcript_and_audio`,并且当前`raw_audio_storage`授权仍然有效。授权撤回后,不再创建新上传批次。家庭所有者、管理员、编辑者和查看者角色均不会自动获得录音播放或下载权。
## 2. 数据模型
| 对象 | 用途 | 状态/关键约束 |
|---|---|---|
| `upload_batch` | 设备一次分片清单及校验汇总 | `PENDING → UPLOADING → VERIFYING → COMPLETED/FAILED/EXPIRED`;设备、通话和幂等键唯一 |
| `media_segment` | 通话内单轨单序号录音对象 | 云端生命周期为`DECLARED → AUTHORIZED → VERIFYING → VERIFIED/REJECTED → DELETED`;另存设备本地密文删除状态、回报时间和失败码 |
| `upload_batch_segment` | 连接重试批次与稳定分片记录 | 同一分片可在失败后进入新批次重试,不生成重复媒体记录 |
| `recording_access_grant` | 指定用户的显式访问授权 | `PLAY`与`DOWNLOAD`分别授权,带用途、授予人、到期和撤销时间 |
| `media_access_log` | 每次播放/下载判定记录 | 保存用户、资源、动作、用途、成功/拒绝/失败、原因和请求编号 |
对象路径由云端按租户摘要、通话编号、轨道、序号和摘要前缀生成,不含长者姓名,也不接受设备自定义路径。数据库只保存对象引用,不保存临时上传或下载URL。
## 3. 设备上传流程
1. 通话结束后,设备为`near`和`far`分片计算SHA-256,提交轨道、序号、编码、内容类型、时长、大小和摘要,并提供16—128字符幂等键。
2. 云端检查设备与通话归属、通话终态、冻结录音策略、当前原录音授权、轨道/序号唯一性和清单结构。
3. 相同幂等键和相同清单返回原批次;同键不同清单或相同轨道序号对应不同元数据返回冲突。
4. 云端只为未验证分片签发15分钟、只能PUT一个私有对象的地址。录音字节由设备直接上传对象存储。
5. 设备调用完成接口后,事务把分片置为`VERIFYING`,并通过Outbox可靠创建`media.verify_requested`任务;重复完成不会重复生成校验任务。
6. Worker流式校验实际大小和SHA-256,再解析音频容器、编码和时长。对象暂时不可见时任务保留`VERIFYING`并按Outbox策略重试;大小、摘要、容器、编码或时长不符时进入`REJECTED`。
7. 全部分片验证通过后批次进入`COMPLETED`。设备查询到分片`VERIFIED`和`local_delete_after`后,才可在该时间之后删除本地加密文件。
8. 设备删除完成后提交`DELETED`回执;删除失败则提交`FAILED`和稳定错误码。云端拒绝过早的成功回执,已成功回执可幂等重放,且回执只表示设备副本已删除,不改变云端已验证对象状态。
`MEDIA_LOCAL_DELETE_GRACE`控制云端校验通过到允许终端删除本地密文之间的保护期,默认1小时,允许范围为大于0且不超过7天。Compose本地联调环境使用1秒以便快速完成闭环;生产环境未显式配置时保持1小时,不能为了缩短联调时间而提前返回删除成功。
当前研发基线实际解析`audio/wav`的PCM 16位格式并核对时长。Opus/Ogg等编码尚未在硬件能力表和分片清单格式中冻结,因此不会被当前API接受,避免把“只相信设备声明”误报为完成编码校验。
## 4. 播放与下载流程
1. 家庭所有者或管理员为家庭内指定用户显式创建`PLAY`或`DOWNLOAD`授权,填写用途和到期时间。编辑者和查看者不能创建授权。管理端可查询最近授权、识别有效/到期/已撤销状态并立即撤销。
2. 查看通话媒体时,服务重新检查账号仍是家庭有效成员、原录音授权仍有效,并确认至少有一种当前有效访问授权。响应仅返回已验证分片元数据,不返回对象地址。
3. 每次播放或下载都调用访问接口,重新检查成员关系、原录音授权、动作对应的独立授权和分片`VERIFIED`状态。
4. 检查成功后签发5分钟单对象地址:播放使用`inline`,下载使用`attachment`;文件名只包含通话编号、轨道和序号。
5. 成功和拒绝都写入`media_access_log`及统一审计。撤销播放授权不会自动撤销下载授权,反之亦然。
## 5. 接口
| 方法与路径 | 用途 |
|---|---|
| `POST /device/v2/conversations/{id}/upload-batches` | 幂等提交分片清单并取得缺失分片上传地址 |
| `POST /device/v2/upload-batches/{id}/complete` | 声明上传完成并可靠触发后台校验 |
| `GET /device/v2/upload-batches/{id}` | 查询批次、分片结果及本地延迟删除时间 |
| `POST /device/v2/media-segments/{id}/local-delete-ack` | 回报设备本地密文删除成功或失败;成功受删除时间门禁约束 |
| `GET /api/v1/subjects/{id}/recording-access-grants` | 所有者/管理员查询最近录音访问授权及状态 |
| `POST /api/v1/subjects/{id}/recording-access-grants` | 显式授予PLAY或DOWNLOAD |
| `POST /api/v1/recording-access-grants/{id}/revoke` | 撤销一条录音访问授权 |
| `GET /api/v1/conversations/{id}/media` | 按授权读取两轨已验证分片元数据 |
| `POST /api/v1/media-segments/{id}/access` | 重新鉴权并取得5分钟播放或下载地址 |
字段、错误码和设备签名头以`openapi/openapi.yaml`为准。
## 6. 删除与验证
主体删除任务的`object_storage`子任务会逐个删除该长者全部媒体对象,并将分片置为`DELETED`、清空对象引用和文件摘要;PostgreSQL子任务同步撤销录音访问授权、脱敏授权用途和访问用途,并清理媒体Outbox正文。任一对象删除失败时隐私请求保持部分失败并继续重试,不会错误显示完成。
本地完整环境运行:
```powershell
pwsh -NoProfile -File .\scripts\smoke-media-upload.ps1
```
脚本验证双轨直传、清单幂等、同键冲突、对象大小/SHA-256、WAV PCM16容器与时长、后台收敛、错误摘要拒绝、本地删除时间门禁、失败回执、成功与幂等回执、角色默认拒绝、授权列表权限、PLAY/DOWNLOAD独立授权、编辑者授予拒绝、撤销立即生效、短期对象访问,以及成功/拒绝访问审计;默认结束后删除全部合成数据库记录和对象。`-KeepFixture`仅供隔离的页面视觉验收,验收结束必须清理夹具。
React通话详情页展示近端/远端已验证分片、播放和下载能力、短期地址说明、按成员/用途/有效期的显式授权、当前有效授权和立即撤销入口。播放地址只保存在当前页面内存中,刷新页面后失效;下载通过一次性页面链接触发,前端不把短期地址写入持久化存储。
录音显式授权创建也使用家庭业务受控操作区。原录音管理表单只负责选择家庭成员、PLAY或DOWNLOAD、1/8/24小时有效期和用途;点击“确认授权”后先冻结长者、接收成员公开编号、单项能力、用途和准确到期时间。页面明确该授权按长者生效,可覆盖有效期内当前及后续可访问的全部已验证原录音,而不是只覆盖当前打开的通话;同一成员原有的同类有效授权会被服务端撤销并替换,家庭角色本身仍不产生录音能力。操作者显式确认后才调用创建接口,取消或切换通话、长者、家庭、工作台不会创建授权。
录音访问授权的“立即撤销”不会再单击即提交。页面先打开家庭业务受控操作区,固定展示长者、被授权成员、授权公开编号、PLAY或DOWNLOAD权限、用途、授予时间和到期时间;提交前必须确认后续不再为该成员签发新地址,并知晓已经签发的地址最长仍可能在5分钟内有效、成员已保存的离线副本无法由平台远程收回。取消只清除当前操作草稿,不调用撤销接口;切换长者或其他家庭业务上下文也会清除旧目标,避免误操作到上一条授权。
已获得独立权限的用户点击具体分片的“播放”或“下载”时,也不会立即签发地址。页面先冻结当前长者、通话公开编号、近端/远端轨道、分片序号和公开编号、时长、大小及用途:播放确认说明5分钟浏览器地址、逐次访问审计和旁听泄露风险;下载确认说明本地声音文件离开平台后的受控保存、转交与销毁责任。只有显式确认后才调用访问接口,服务端仍会重新核验家庭成员关系、动作对应的PLAY或DOWNLOAD授权、当前原录音授权和分片VERIFIED状态。切换通话、长者、家庭或工作台会清除旧分片草稿。
该页面采用低强度真实链路检查:使用真实本地OIDC登录和临时授权记录,只验证目标绑定、影响确认、取消关闭、零撤销或地址签发请求和数据库不变,不实际撤销业务数据或生成可访问地址。其中撤销页面夹具的通话快照未给当前账号原始录音播放权,媒体接口返回403被视为预期权限门禁;地址签发页面则使用最小PLAY/DOWNLOAD授权、当前原录音授权和一条已验证分片,证明按钮确实可达。除此之外的控制台错误、请求失败或页面溢出仍会使检查失败。
模拟终端的`media`场景现在会执行清单声明、对象直传、提交校验、轮询批次、等待删除保护期和`DELETED`回执。它不会删除`SIM_MEDIA_FILE`指向的操作人员文件,只把已上传内容视为内存中的设备密文副本并在回执前释放。生产保护期超过模拟器等待上限时,模拟器只报告计划删除时间,不发送过早成功回执。