Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/sessions/261008-local-upload/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 本地媒体上传:A 卡安全基线

## 范围与批准边界

依据 2026-10-08 已批准的 v2(D16–D19、R1–R8)与最新授权“拆卡完整落地我们的设计方案”。本文件只持久化已批准的当前设计与跨卡 HTTP 合同;不是完整上传项目或生产启用许可。

A 卡交付:`cache.db` 内独立上传身份/期限/撤销记录;上传根任务与旧 `task_status.view_token` 分离;Resolver 只从上传记录解析 `upload_` capability;缓存/任务清理只在分享有效期间保护必要成果与根记录。`VTA_UPLOADS_ENABLED` 缺失或不是 `true` 时,上传分享读取拒绝,且写一条可 grep 的 `UPLOAD_DISABLED` 日志。开关关闭不改变清理保护。当前没有上传 HTTP 接收端、真实本地 ASR worker、上传 UI、生产额度或恢复证明。

主身份仍是现有 Bearer 用户。正文链接公开读取;提交归属由 owner 持有。每次正式上传独立成果,不跨上传去重;同一 owner 与同一幂等 key 是同一意图。上传开关不写入数据卷配置;没有完整恢复域证明,部署保持关闭。不得据代码测试宣称已授权生产部署。

## SQLite 与生命周期

单一存储仍是 `CacheManager`。新增 `local_uploads` 表含:`upload_id`、`owner_user_id`、幂等 key 和 key 创建时间、规范化请求 metadata 的 SHA-256 指纹、接收态、`retention`、唯一 root task / media / 独立 `upload_` token、创建时间、`expires_at`、write-once `revoked_at`、`error_code`。`UNIQUE(owner_user_id, idempotency_key)` 和 `BEGIN IMMEDIATE` 使并发重复登记只产生一个持久 receipt;同 key 元数据不同拒绝,不读新 body、不建第二个 root。

上传 root task 仍用现有 `task_status` 做进度和终态真相。由于旧列 `view_token TEXT NOT NULL`,上传根行写 `view_token=''`,`platform='local_upload'`;公开 token 只存 `local_uploads.view_token`。空 token 与 `upload_` token 都不准旧 `get_task_by_view_token` 或 audit 归属 legacy fallback 解析。写入撤销后不能复活;DB trigger 和 `COALESCE` 写入锁住 `revoked_at` 单调性。

接收 key 格式为 `<UTC_epoch_ms>-<UUID>`。首次登记只接受当前时刻之前 24 小时内的 key,未来最多容许 5 分钟时钟偏差;超过 24 小时的已有 owner/key 仍可查原记录,记录退休/清除后同 key 不得重新受理。此窗口是幂等记录生命周期,不是分享期限。当前 A 不做记录 GC;后续维护只能在确认查询/历史消费者及唯一记录窗口后回收。

`30d` 从 root 首次 `success` 或 `failed` 终态的 SQLite `completed_at` 固定加 30 天;未到终态时 `expires_at=NULL` 不等于长期,尚未终态的 root 只呈现处理中。`never` 才以 `expires_at=NULL` 表示不自动到期。子任务、重复终态写和重启不续期。撤销时间只写一次;关闭分享不取消已接受处理,也不专属擦除正文。

Resolver 每次新请求都查独立上传记录并核对开关、撤销和截止边界(`expires_at <= now` 已失效);它不经旧 token fallback。有效 `never` 与有效 `30d` 均从既有 CacheManager 读真实缓存。失效、撤销、禁用均不返回新正文;失效内容随后按原全局清理规则处理。上传 reprocess 业务尚未实现,`upload_` token 不能走既有 task owner fallback,当前拒绝重处理。

## 清理与数据边界

- `cleanup_old_cache` 在现有媒体锁路径下检查有效上传 root 与 media 关系;有效短期和长期正文都保护,跟 `VTA_UPLOADS_ENABLED` 无关。撤销/到期后,缓存按已有 `video_cache.updated_at` 全局年龄规则回收。URL 缓存仍走原规则。
- `cleanup_task_status` 在 `BEGIN IMMEDIATE` 内再次检查 root 的有效分享资格;有效 root 留存,audit snapshot 因现有 `task_exists` 依赖仍可解析。inactive 上传和 URL task 按原 task retention 及快照过期次序清理。Keyset 扫描越过被保护记录,避免有效长期记录阻塞后续批次。
- 审计 ownership 不接受 `upload_` token 的 legacy fallback;有效分享所需根 task/snapshot 留存由现有 audit 消费者关系维持。历史展示仍由后续 B/D 卡实现。
- `data/temp` 中原媒体、转码副本和任务暂存文件仍是临时输入,不因长期正文分享而保护;现有 `TempFileManager` 保持其清理行为。

## 跨卡 HTTP 合同(B/C/D/E 消费)

### 请求与幂等

- 原生 raw `File`/`Blob` body;`Content-Type: application/octet-stream`;现有 `Authorization: Bearer ...`。
- `Idempotency-Key: <UTC_epoch_ms>-<UUID>`;服务器核验 24 小时受理窗口与最多 5 分钟未来时钟偏差。窗口内同 owner/key 唯一记录不能 GC;已有 key 的 receipt 仍可查;过窗且唯一记录已退休时拒绝,不能重新受理。
- `X-Upload-Metadata` 为 base64url 编码的 UTF-8 compact JSON,必须有界;字段 `filename`、`byte_size`、`title`(可空)、`source_url`(可空)、`retention`(`30d` 或 `never`)、`processing_options`(沿现有处理选项)。不得含本地路径、`user_id` 或 public token。
- 首次提交和接受动作必须由真实服务端边界验证 metadata;重复相同意图不读新 body、不重入队,只返回原 receipt;同 key metadata 变化返回 409。未知回执只查询,不自动换 key 或重发。

### 响应

- `GET /api/uploads/capabilities`(Bearer):`{enabled:boolean, default_retention:'30d', retention_options:['30d','never'], limits:{max_file_mib,max_media_hours,receive_concurrency,upload_temp_budget_mib}}`。四项额度必须正且有限、来自经过真实消费环境验证的配置;当前生产值未知/未测,缺一项即 `enabled=false`。不得用示例值声称已测。
- `POST /api/uploads`:只有数据库正式接受后才返回 202;同意图返回已有 receipt,变化 metadata 返回 409。FastAPI 错误必须明确 status/detail,不得假 202。
- `GET /api/uploads/by-idempotency-key/{key}`(Bearer/owner):已存在 receipt 或 404;不能借查询创建任务。
- receipt:`upload_id`、`state`(接收阶段,不复制 `task_status`)、`task_id`(未正式受理时 null)、`view_token`(正式受理后独立 `upload_` token,否则 null)、`retention`、`expires_at`(终态前与 `never` 为 null)、`share_active`、`error_code`(无错误为 null)。处理进度继续由既有 `GET /api/task/{task_id}` 承载。
- `DELETE /api/uploads/{upload_id}/share`(Bearer/owner):`{upload_id, share_active:false, revoked_at}`。重复关闭返回原截止,不得复活。关闭/到期后所有新 public read 和新 reprocess 拒绝;已经授权的传输和已接受处理可继续。
- 本人历史增加 `source=upload` 过滤,先过滤再分页;upload item 可含 `upload_id/retention/expires_at/share_active/revoked_at/title/source_url`。bodygate 未实现前不得提供旁路摘要。

## 不变量与真实验收入口

| 不变量 | 实现入口 | 锁定测试 |
|---|---|---|
| owner/key 唯一、窗口/时钟偏差、metadata conflict、旧 receipt 查询与过窗拒绝 | `cache_manager.py::register_local_upload` | `tests/unit/test_local_upload_policy.py`(临时真实 SQLite,含并发登记) |
| root `view_token=''`、独立 `upload_` token;blank 与上传 token 不走旧别名 | `cache_manager.py::accept_local_upload/get_task_by_view_token`、`audit.py::check_view_token_ownership` | `test_upload_task_uses_blank_legacy_column_and_separate_upload_token`、Resolver 与 ownership 用例 |
| success/failed 首终态固定 30 天;never 不到期;撤销 write-once | `cache_manager.py::update_task_status/revoke_local_upload` | `test_root_terminal_sets_fixed_30_day_expiry_for_success_and_failure`、`test_never_expiry_and_revocation_are_write_once` |
| Resolver 对 active 30d/never 读真实缓存;missing/off/revoked/expired 不读;`UPLOAD_DISABLED` 可 grep | `view_token_resolver.py::_local_upload_task_info` | `test_resolver_reads_active_upload_and_denies_disabled_revoked_or_expired`、`test_upload_progress_token_resolves_before_30_day_terminal_clock` |
| 有效产物不被 cache/task/audit 清理删除;开关 off 不影响保护;关闭后恢复原清理;媒体仍临时 | `cleanup_old_cache/cleanup_task_status`;现有 `task_exists` 与 `TempFileManager` | local policy cleanup/controlled ordering tests、`tests/cache/test_task_status_cleanup.py`、`tests/cache/test_cache_cleanup.py` |
| URL 读、去重、history 与 task 行为不改变 | 原有 URL routes/CacheManager | `tests/unit/test_view_token_resolver.py`、`test_api_routes.py`、`test_history_routes.py` 及 cache cleanup 套件 |

当前真实开发验证入口:`uv sync --frozen`;重复五次 `uv run --frozen pytest -q tests/unit/test_local_upload_policy.py tests/unit/test_view_token_resolver.py tests/cache/test_task_status_cleanup.py`;完整 gate `make test`。对环境变量依赖的测试必须在实际消费 Resolver 的环境运行。部署/systemd 恢复及容量验证仍是上线前置条件;本卡不触及生产 unit、数据卷或服务。

## 非目标与依赖

A 不做 HTTP 接收/流式限额、队列原子交接、ASR/LLM 接线、上传完整 reprocess、网页、部署/备份恢复实测、生产容量验证、source URL fetch 或原媒体长期留存。B 消费该 store 与 Resolver;C 消费 root/终态与媒体清理合同;D 消费本节 wire/history 合同;E 证明真实 producer 到 HTTP/ASR/正文消费者;F 证明独立恢复域与单向关闭。

不增加账户平台、`LocalUploadService` 转发层、outbox、broker、重试/fallback、外部撤销系统或新配置源。没有 `config.py` 加载器;`VTA_UPLOADS_ENABLED` 是部署环境变量,故不写入 `config/config.example.jsonc` 或数据卷配置。没有正有限额度和恢复证明时,B 必须报告关闭,而不是启用默认占位值。
28 changes: 28 additions & 0 deletions docs/sessions/261008-local-upload/progress/A-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# A 卡进度

## 当前阶段

implementing;store/Resolver/cleanup 代码已提交为 `132daf96`,窄测五连绿、指定回归与 `make test` 均通过;blank/source/cleanup 三项变异均由 AssertionError 命中且恢复后绿。待设计/六里程碑文件提交、push 与 PR 独立审查。HTTP接收、worker、网页、生产额度和恢复演练尚未实现,不能启用生产上传。

## 本段结论(≤3句)

- A 使用现有 `CacheManager` 作为 store,新增 `local_uploads`;不增加 Service 转发层或账户/队列平台。
- Resolver 通过独立 `upload_` token 读取,旧 NOT NULL token 列保持空字符串;禁用、撤销、到期拒读且不改变有效成果清理保护。
- 最终窄测连续五次通过,指定 URL/history/cache/task/媒体锁清理回归与 `make test` 全量 gate 均通过;blank lookup、上传 source fallback、active-cache cleanup predicate 的移除各自触发普通 AssertionError,恢复后对应测试各绿。历史 `tests/cache/test_cache_cleanup.py` 会删除仓库 fixture,已改成 `tmp_path` 且消费 producer 返回的路径。

## 关键决策与否决方案

- 24 小时 key 窗口、5 分钟未来时钟偏差;重复 owner/key 通过 `BEGIN IMMEDIATE` 复用既有记录,metadata 指纹不同直接拒绝。
- 30 天自 root 首个 success/failed 的 `completed_at` 固定起算;never 仅以显式选择无到期,撤销通过 SQLite trigger write-once。
- 不将 `VTA_UPLOADS_ENABLED` 放进数据卷 JSON;生产额度、systemd恢复域未验证前保持关闭。
- 上传 reprocess 暂不接线,拒绝走 task_status owner legacy fallback;原媒体仍走 TempFileManager 临时清理。

## 本段结论:推送与 draft PR

- 代码提交 `132daf96`、设计/里程碑提交 `cbad5ac6` 均已 push;PR #201 为 OPEN draft,当前 tip `cbad5ac62c5e3a1aeb7663a00e1d0385004d914c`。
- CI run `37764505752` 的 run conclusion 为 SUCCESS,quality/draft-gate/ledger job 为 SUCCESS;primary 为 SKIPPED(draft),不能等同独立主审或 ready。
- 实现与本地验证完成,仍待 Pi lead 独立审查;未 ready/merge、未碰生产。

## 下一步唯一动作

由 Pi lead 独立审查 PR #201 并决定后续 gate 节点;执行器不把 PR 标 ready、不 merge、不启用生产。
23 changes: 23 additions & 0 deletions goals/local-upload/M1-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
lane: local-upload
id: M1
slug: baseline
status: 实现中
owner: delegate implementer dlg-20261008-094534-0fd910
order: 1
priority: 高
depends_on: []
merged_pr: null
---

# 里程碑进度:local-upload/M1:上传身份、分享资格与清理基线

- **预期产出**:cache.db 持有独立上传身份/期限/撤销记录;旧 NOT NULL `view_token` 不再承载公开上传 token;实际 Resolver 与 cache/task/audit/temp 维护消费者按资格保护。
- **当前范围**:A 卡 guard-only 基线。不包含 HTTP 接收、真实 ASR worker、网页、生产额度和恢复证明;未合并、未部署、未启用。
- **关键决策**:用现有 `CacheManager` 做 store;`view_token=''` 只作旧列占位,公开 `upload_` token 只从新表解析;`VTA_UPLOADS_ENABLED` 缺省/非 true 即关闭读取,不影响清理保护。
- **推进前必须拿到的证据**:
- [x] 卡树 `uv sync --frozen` 后,窄测连续五次:`uv run --frozen pytest -q tests/unit/test_local_upload_policy.py tests/unit/test_view_token_resolver.py tests/cache/test_task_status_cleanup.py`,五轮 exit=0;环境:本地隔离 worktree 与临时 SQLite,日志 `/tmp/vta-upload-A-narrow-final-{1..5}.log`。
- [x] `make test` 完整 gate exit=0;URL Resolver、API、history、cache/task cleanup 与媒体锁竞态整文件回归共 196 tests,exit=0;环境:当前 card worktree。全量日志 `/tmp/vta-upload-A-make-test-final.log`。
- [ ] Resolver 开关测试同时经过当前进程和真实子进程,使用同一份临时 SQLite 验证变量缺省与显式 true;CI 和裸 shell 需运行。当前仓无 upload systemd consumer;不得把本地结果声称为 systemd/生产验证。
- [ ] 独立审查与 PR run/job 实际 conclusion;draft 的 SKIPPED 不算 primary 通过。
- **完成条件**:上述证据全部来自真实 Resolver 与维护入口,代码位置/测试锁定成对;PR merge 之前由 lead 验收。生产启用仍依赖 M2–M6 的部署、容量及恢复门。
23 changes: 23 additions & 0 deletions goals/local-upload/M2-intake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
lane: local-upload
id: M2
slug: intake
status: 未开始
owner: delegate implementer dlg-20261008-094534-0fd910
order: 2
priority: 高
depends_on: [local-upload/M1]
merged_pr: null
---

# 里程碑进度:local-upload/M2:认证接收与幂等回执

- **预期产出**:Bearer 认证的能力查询、raw octet-stream 接收、有限 metadata header、真实额度准入、owner/key receipt 与诚实 HTTP 状态。
- **当前范围**:B 卡;不把 A 的 SQLite store 方法误报为已存在 HTTP 接收端。
- **关键决策**:沿用 `docs/sessions/261008-local-upload/design.md` 的 wire 合同;key 窗口24小时、未来容差5分钟;同意图不消费新 body、不重入队,变化 metadata 409。
- **推进前必须拿到的证据**:
- [ ] 真实 FastAPI/ASGI 与临时 cache.db 上由 client 发 raw 文件字节及所有 headers;断言 handler 实收字节、metadata 解码、commit receipt 与真实 task/queue producer 参数。跨进程 payload 保留 producer fixture。
- [ ] 无开关、关闭开关、任一额度缺失/无效时实际返回 disabled 并拒绝接收;四项额度全为正有限数才测 enabled。生产测量前不填真实容量结论。
- [ ] 同 key 重放时 consumer 证明没有读取新 body、没有第二次 queue 入队;查询只读回原 receipt;丢回执不自动生成新 key/重传。
- [ ] 在 CI、无会话身份裸 shell 和获准隔离 systemd unit 分别验证真实消费环境变量与接收行为。systemd/生产改动另需部署授权。
- **完成条件**:202 只在持久正式接受后返回;未知/失败不假成功;queue/SQLite 交接窗口由真实入口测试锁死,不加 retry/fallback/outbox。
23 changes: 23 additions & 0 deletions goals/local-upload/M3-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
lane: local-upload
id: M3
slug: worker
status: 未开始
owner: delegate implementer dlg-20261008-094534-0fd910
order: 3
priority: 高
depends_on: [local-upload/M1, local-upload/M2]
merged_pr: null
---

# 里程碑进度:local-upload/M3:本地输入进入既有转录 worker

- **预期产出**:本地 owned media 按唯一 root task 接入既有 `process_transcription`、ASR、LLM、通知、终态与临时文件生命周期。
- **当前范围**:C 卡;不另建 ASR/LLM/通知状态机,不实现第二套重试/预算,不把公开 upload token 写入旧 `task_status.view_token`。
- **关键决策**:复用现有处理链;root首个 success/failed 起算固定期限,子任务/重启不续期;原媒体及中间文件仍按既有 finally 清理。
- **推进前必须拿到的证据**:
- [ ] 真实 `process_transcription` 消费临时文件与 SQLite root;只替换 ASR/LLM/通知外部边缘。保留 producer fixture 并断言实际 ASR 参数/argv、文件字节、root id、终态与通知 payload。
- [ ] success/failure/invalid result 均有诚实终态;stop-share 不取消已接受处理、不专属删正文;重启/迟到写不能改变 root TTL 或复活撤销。
- [ ] queue、数据库、ASR/LLM 失败通过具名状态/日志可见;不吞错、不假成功、不自动重试或切换引擎。
- [ ] 在 CI、裸 shell、获准 systemd 隔离 unit 测真实环境变量和 worker 启动消费;不碰生产配置/数据。
- **完成条件**:真实处理者消费的是接收器写出的媒体与选项,阶段与 URL 路径保持一致;正文资格和任务进度分离。
23 changes: 23 additions & 0 deletions goals/local-upload/M4-web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
lane: local-upload
id: M4
slug: web
status: 未开始
owner: delegate implementer dlg-20261008-094534-0fd910
order: 4
priority: 中
depends_on: [local-upload/M1, local-upload/M2, local-upload/M3]
merged_pr: null
---

# 里程碑进度:local-upload/M4:上传、回执与本人历史网页

- **预期产出**:原生单文件 picker/drop、metadata headers、期限选择、真实 receipt/处理中/结果状态与本人 history source filter。
- **当前范围**:D 卡;不得从 mock-only fixture 发明 wire 形状,不改旧 URL 任务页面规则。
- **关键决策**:沿用设计文件中的 HTTP 字段;默认30天、长期显式选择 never;进度继续走既有 task API;source URL 仅展示,不 fetch。
- **推进前必须拿到的证据**:
- [ ] 浏览器/harness 调用真实 API,断言 producer 发出的 raw Blob 字节、headers 和完整服务端 receipt;保留实际请求 fixture。
- [ ] picker/drop 同路;100% 传输不等于正式接受;覆盖 401、网络未知、duplicate receipt、处理中、失败、关闭和到期状态。
- [ ] 真实 history query 先按 source 过滤再分页;断言实际 ID 集、total、页边界与 owner 隔离;未实现 bodygate 时无摘要旁路。
- [ ] filename/source 输入经真实模板安全渲染;恶意路径、Unicode、引号、无效 URL、键盘和小屏测试。CI browser/API 环境运行;systemd验证仅随另行授权部署。
- **完成条件**:浏览器状态只反映服务端 receipt/进度事实;失效 token 仍由服务端拒绝,文案不许承诺回收通知副本。
Loading
Loading