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
8 changes: 7 additions & 1 deletion config/config.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@
"cache_retention_days": 360, // 缓存保留天数(每日定时清理超期缓存记录及文件),0 表示永久保留
"task_status_retention_days": 180, // task_status 表终态(success/failed)任务记录保留天数(每日定时清理),0 表示永久保留。注意:/view 链接依赖 task_status 行解析,本值不应短于 cache_retention_days,短于时按缓存保留期执行(钳制并记 warning);cache_retention_days 为 0(缓存永久保留)时本清理会被跳过
"audit_log_retention_days": 180, // 审计日志保留天数(每日定时清理 audit.db 超期记录),0 表示永久保留
"max_download_size_mb": 4096 // 单文件下载大小上限(MB),0 表示不限制,防止超长视频填满磁盘
"max_download_size_mb": 4096, // 单文件下载大小上限(MB),0 表示不限制,防止超长视频填满磁盘
"upload_limits": { // 未经容量实测的上传限制留 null;缺少任一正有限值时上传能力关闭
"max_file_mib": null,
"max_media_hours": null,
"receive_concurrency": null,
"upload_temp_budget_mib": null
}
},

// ============================================================
Expand Down
9 changes: 5 additions & 4 deletions docs/sessions/261008-local-upload/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

依据 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、生产额度或恢复证明。
A 卡交付:`cache.db` 内独立上传身份/期限/撤销记录;上传根任务与旧 `task_status.view_token` 分离;Resolver 只从上传记录解析 `upload_` capability;缓存/任务清理只在分享有效期间保护必要成果与根记录。`VTA_UPLOADS_ENABLED` 缺失或不是 `true` 时,上传分享读取拒绝,且写一条可 grep 的 `UPLOAD_DISABLED` 日志。开关关闭不改变清理保护。B 已接入 raw-stream HTTP、owner receipt、持久 admission、dispatcher 与既有本地媒体 ASR 路径;默认开关关闭,生产额度、容量实测、恢复证明及生产启用均仍未完成/授权。

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

Expand All @@ -14,7 +14,7 @@ A 卡交付:`cache.db` 内独立上传身份/期限/撤销记录;上传根

上传 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;后续维护只能在确认查询/历史消费者及唯一记录窗口后回收。
接收 key 格式为 `<UTC_epoch_ms>-<UUID>`。新正式受理截止于 key 创建时间后 24 小时:到达截止点(now >= created_at + 24h)即拒绝;key 最多容许 5 分钟未来时钟偏差。超过 24 小时的已有 owner/key 仍可查原记录;过期 `receiving` 草稿及其暂存字节可退休/清理,key 时间戳仍保证其不能重新受理。此窗口是幂等记录生命周期,不是分享期限。`receiving` 不是已接受任务,不得返回 202;相同 key 重放只回原阶段回执、不读取 body 或重新入队,未知回执只查询,不自动换 key/重传。

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

Expand All @@ -39,7 +39,7 @@ Resolver 每次新请求都查独立上传记录并核对开关、撤销和截
### 响应

- `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。
- `POST /api/uploads`:root、accepted 状态和媒体映射在 `BEGIN IMMEDIATE` 事务中写入,并在同一无 await SQL 临界步骤内仅调用一次有界队列 `put_nowait`;事务提交成功后才正式受理并返回 202。SQLite 与进程内队列并非共同事务:队列 put 成功而 SQLite commit 失败时允许留下stale队列项,但dispatcher必须先核验持久admission/root/media映射、在 PROCESSING/executor.submit/ASR 前拒绝它。QueueFull自然回滚未受理SQL,不依赖事后撤回已accepted root。相同意图返回原回执(包括仍为 receiving 的阶段回执),变化 metadata 返回 409;receiving/failed/未知回执均不伪装为 accepted/202。FastAPI 错误必须明确 status/detail。
- `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 拒绝;已经授权的传输和已接受处理可继续。
Expand All @@ -50,13 +50,14 @@ Resolver 每次新请求都查独立上传记录并核对开关、撤销和截
| 不变量 | 实现入口 | 锁定测试 |
|---|---|---|
| owner/key 唯一、窗口/时钟偏差、metadata conflict、旧 receipt 查询与过窗拒绝 | `cache_manager.py::register_local_upload` | `tests/unit/test_local_upload_policy.py`(临时真实 SQLite,含并发登记) |
| root/accepted+一次put_nowait提交顺序、commit失败stale item拒绝、过期receiving retirement与源文件所有权的两个真实顺序 | `cache_manager.py::accept_local_upload/cleanup_expired_local_upload_receiving`、`api/services/transcription.py::process_task_queue` | `tests/integration/test_upload_intake.py::test_http_queue_handoff_precedes_durable_acceptance_commit`、`test_sql_commit_failure_leaves_stale_queue_item_for_dispatcher_to_drop`、`test_receiving_cleanup_rechecks_after_accept_wins_and_preserves_owned_file`、`test_receiving_cleanup_retires_before_unlink_and_accept_loses` |
| 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 的窄测与 `make test`;B 真实 HTTP/SQLite/dispatcher 入口为 `uv run --frozen pytest -q tests/unit/test_upload_routes.py tests/unit/test_upload_dispatch.py tests/integration/test_upload_intake.py`。B 测试覆盖真实 Bearer、raw body 实际字节与服务端 SHA-256、容量、queue put 与 SQL commit 双顺序、cleanup/accept 两顺序、真实 stale dispatcher、断开、重复/丢回执与撤销。所有生产额度仍为 null/未知,`VTA_UPLOADS_ENABLED` 缺失时 intake 与 share 都关闭;部署/systemd 恢复及容量验证仍是上线前置条件,本卡不触及生产 unit、数据卷或服务。

## 非目标与依赖

Expand Down
102 changes: 102 additions & 0 deletions docs/sessions/261008-local-upload/progress/B-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# B 卡进度

## 当前阶段

implementing;完成第一份独立可运行单元:bounded metadata decoder 与Unicode/字段约束测试通过。尚未实现 HTTP 接收、队列/SQLite 原子交接、worker readiness、实际integration验证。

## 本段结论(≤3句)

- metadata 使用有界 UTF-8 base64url JSON;显示文件名规范化为 basename,不作为路径;处理选项沿用既有 normalize_processing_options。
- `tests/unit/test_upload_routes.py` 通过,初始失败为值差异 AssertionError,修复后全绿。
- 尚无验证失败归因或生产环境结论。

## 关键决策与否决方案

- 不消费客户端文件hash;元数据只声明正字节数,正文由服务端流式计数/hash。
- source_url 只接受无userinfo的 http/https 文本,不抓取。

## 下一步唯一动作

扩展 CacheManager 持久 admission/root/media mapping 与 runtime 限额/预留,再接上真正 HTTP stream 和 dispatcher 测试。

## Milestone 2:流式HTTP与持久队列交接

### 本段结论(≤3句)

- 已实现 GET capabilities、raw `application/octet-stream` POST、owner receipt/DELETE、服务端流式 byte count/SHA-256、metadata边界、unique owner/key、24h截止与过期receiving清理。
- H0实现曾在SQLite root/accepted/mapping commit之后才调用队列 `put_nowait`;H1续补将同步 `put_nowait` 移入同一事务提交前,dispatcher仍在 PROCESSING/executor.submit 前核验持久受理与 root/media/path 映射,local_upload 走现有ASR路径且不经过URL downloader。
- 真实 ASGI+临时SQLite/dispatcher窄测:`tests/unit/test_upload_routes.py tests/unit/test_upload_dispatch.py tests/integration/test_upload_intake.py` 全绿(32 passed);尚未跑全仓 make test、裸shell/systemd验证或 CI。

### 关键决策与否决方案

- upload limits 只从现有 `storage.upload_limits` 消费,示例四值均 null;`VTA_UPLOADS_ENABLED` 独立于数据卷配置,只有env、正有限额度及实际存活的 process_task_queue 同时满足才报告 enabled。
- body接收目录在开始读取前持久记录并由 TempFileManager active 集保护;未移交时由接收者清理,移交后worker清理;worker future完成释放原始输入的磁盘/inflight预留。
- `receiving` key在创建+24h截止前保留唯一;正式接受在截止点及以后显式410,过期receiving草稿和未移交暂存路径由maintenance退休;不自动重试/换key。
- 队列预满时在读body前拒绝;H0的事后撤回补偿仅属旧实现,H1已删除。QueueFull由事务回滚留在receiving/失败回执;commit失败时已入队的stale item由dispatcher durable gate丢弃。

## Milestone 3:全仓验证与交付

### 本段结论(≤3句)

- `make test` 完整门禁 exit 0;输出到 `/tmp/vta-upload-B-make-test.log`,pytest 到 100%,3 个既有用例 skip,无失败。
- 同一未设置 `VTA_UPLOADS_ENABLED` 的消费测试分别在 `env -i` 裸shell与临时 systemd user unit `vta-upload-B-env-verify-1791469452-1251284.service` 运行成功;未修改真实 unit/生产配置。
- commit `9ed74d9b5997aea4acbd3722b2cd6db537571faa` 已 push,draft PR #202:`https://github.com/zlxlabs/VideoTranscriptAPI/pull/202`;本地树clean。CI / 独立主审由lead托管,尚未ready/merge。

### 关键决策与未验证项

- 上传默认保持关闭,示例四项生产额度均为null;本地测试额度只存在临时 fixture,不作为生产容量结论。
- 真实生产 systemd unit、ASR服务容量、恢复域和部署仍未验证;本卡未触碰真实配置、数据、通知或生产ASR。

## Milestone H1:补齐 R3 与接收文件所有权顺序

### 当前阶段

implementing;H1最小顺序修复已提交并push至原PR分支,随后完成窄测、A/URL回归及全量make test。PR保持draft,CI/独立审查由lead托管。

### 本段结论(≤3句)

- H0真实HTTP/SQLite/queue handoff负对照观察到commit前状态已是accepted/root queued;H1在H0上四个候选producer/consumer顺序测试实际失败均为AssertionError,修复后四项通过。
- 当前H1将一次同步`put_nowait`从route移入`CacheManager.accept_local_upload`事务回调;commit失败会留下stale queue payload,由dispatcher在PROCESSING/submit/ASR前拒绝。事后cancel acceptance方法和callsite已删除。
- receiving cleanup现在先于unlink在`BEGIN IMMEDIATE`中重读、核验owner-key时间/仍receiving/路径未变并删除记录,commit后才unlink;两个controlled order测试分别锁定accepted文件保留和retire先赢。

### 关键决策与验证

- 新参数`enqueue`只服务HTTP队列跨SQLite原子发布边界;callback同步调用且无await,既有A store-level direct acceptance仍可不带queue callback。没有新增服务层/状态/锁/重试/fallback。
- 真实测试位于`tests/integration/test_upload_intake.py`:队列put hook用独立SQLite连接观察未提交receiving/root-none和实际payload;commit fault时实际queue收到payload,真实dispatcher drain后没有PROCESSING/executor submit/ASR;cleanup/accept两顺序均使用真实临时SQLite与文件字节,Pause仅位于测试fixture。
- H0红验命令:`uv run --frozen pytest -q tests/integration/test_upload_intake.py::test_http_queue_handoff_precedes_durable_acceptance_commit tests/integration/test_upload_intake.py::test_sql_commit_failure_leaves_stale_queue_item_for_dispatcher_to_drop tests/integration/test_upload_intake.py::test_receiving_cleanup_rechecks_after_accept_wins_and_preserves_owned_file tests/integration/test_upload_intake.py::test_receiving_cleanup_retires_before_unlink_and_accept_loses`;H0四项均以AssertionError显示期望receiving/null或文件存在、实际accepted/root或文件已删;H1同四项4 passed。
- H1窄测`uv run --frozen pytest -q tests/unit/test_upload_routes.py tests/unit/test_upload_dispatch.py tests/integration/test_upload_intake.py`:35个测试点通过;另含A store/Resolver、URL API/history、runtime及maintenance的回归命令退出0。
- H1代码提交`5b1f3526fa958deffd704ba43cbc13be47db95df`已push;四个H0红测在H1已绿,H1末尾窄测+A/URL回归退出0。
- 按红验安全要求,在代码已提交后分别临时把enqueue移到SQL commit之后、把unlink移到SQLite retirement之前;对应producer/consumer断言各自以AssertionError变红,恢复单个顺序后各自绿。变异输出记录在`/tmp/vta-upload-H1-r3-mutation.log`及`/tmp/vta-upload-H1-cleanup-mutation.log`。
- H1 `make test` 与 `uv run --frozen pytest -q -rs tests` 均exit0;3个skip均因`tests/unit/test_capswriter_samples_total_contract.py`缺少真实preflight fixture。完整结果记录在`/tmp/vta-upload-H1-dlg-20261008-151543-50422d-*`。

### 下一步唯一动作

push本进度证据并写终态报告;PR保持draft交lead做H0..H1增量review和CI,不ready/merge。

## Milestone H2:公开处理选项透传

### 当前阶段

implementing;已完成真实 HTTP→持久 SQLite→runtime queue→dispatcher→捕获 consumer 的参数修正与窄测。

### 本段结论(≤3句)

- producer 现在在同一 SQL 临界生成的 queue payload 中发布已写入 task/request metadata 的规范化 `processing_options`,consumer 收到的值不再回退为全开启。
- 新增真实全关、混合、缺省三组边界测试,并断言 HTTP body/header、SQLite、queue payload 与 `process_transcription` 参数一致;旧 H1 交接和标准处理选项回归保持通过。

### 公开字段与实际消费映射(机械对照 decoder/normalize keys)

| HTTP metadata | 持久记录 | queue payload | 实际 consumer 字段/用途 |
|---|---|---|---|
| `filename` | `local_uploads.filename`、`request_metadata` | 无 | `metadata_override.title` 缺省回退与展示 |
| `byte_size` | `local_uploads.byte_size` | 无 | 接收大小/哈希与临时预算验收 |
| `title/source_url/retention` | task title/url、upload source/retention、request metadata | `url`=source_url | consumer 的 `url`/title;retention 供回执分享期限 |
| `processing_options.{calibrate,summarize,infer_speaker_names,chapters,contradiction_scan}` | task `processing_options`、request metadata | `processing_options` | dispatcher normalize 后传 `process_transcription`,控制对应处理层 |

### 复审接受不修

- P2 setup 故障已真实返回 HTTP 500 并释放资源;P3 SQLite commit 故障仍保留 `receive_incomplete` 诊断名;两项均非本卡阻断,不改接收接口或添加防御 catch。

### 下一步唯一动作

提交并推送 H2 小修;保持 draft,交 lead 做 H1→H2 增量复审与正式 CI,不 ready/merge。
Loading
Loading