Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .codemap/codemap.html

Large diffs are not rendered by default.

99 changes: 40 additions & 59 deletions .codemap/codemap.md

Large diffs are not rendered by default.

259 changes: 75 additions & 184 deletions .codemap/modules.json

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions DEVELOPMENT_LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Development Log

## 2026-08-24 稳定 V1 P1.3a 任务状态与切片原子性

- `PATCH /api/tasks/{task_id}/status` 改为显式允许表 + `BEGIN IMMEDIATE` 条件更新;空任务不能直接标记完成,并发状态变化不会被旧请求覆盖。所有内部状态写入同时拒绝已永久删除任务。
- 自动流水线在每个步骤开始前、处理完成后和最终 READY 写入前检查取消;取消后任务稳定进入 `CANCELLED`,Job 进入 `cancelled`,重试仍可从流水线起点恢复。
- 自动流水线成功后稳定停在 `READY_TO_PUBLISH`,不再在同一次 run 内立刻覆盖为 `COMPLETED`;这表示内容已准备好,但仍等待用户在发送中心人工确认。
- Task 步骤状态、切片批次和最终 READY 写回全部绑定当前 Workflow Job 的 owner、token、未过期 lease 与取消标记;旧 Worker 或锁等待期间过期的执行不能覆盖新状态。
- 公开 Task 取消会在同一 SQLite 事务中请求活跃自动 Job 停止;READY 后取消只清理 `provider_response.workflow_job_id` 明确属于本轮且尚未发布的记录,不会误伤其他排期。
- `running + cancel_requested` 在子进程退出或 lease 过期时会收敛为 `cancelled`,不再永久卡在运行态。
- 任务汇总文件写入改为非遮蔽降级:磁盘或源文件异常只补充日志,不会覆盖原始业务失败或取消原因。
- `cut_run` 编号分配放入 `BEGIN IMMEDIATE`;每个批次使用 `run_序号_id` 独立输出目录,避免并发/重试覆盖同名视频。
- 一个批次的全部 `output_clip`、旧版本停用和新版本激活在同一个 SQLite 事务内提交;中途失败整批回滚并保留旧 active,较老批次即使最后完成也不能覆盖已完成的较新批次。
- `cut_run` 创建与 Task=`cutting` 同事务,Task 终态只允许从当前 `cutting` CAS 写入;用户或其他流程刚更新的状态不会被晚到的切片结果覆盖。
- Pytest 增加 session 级隔离数据库初始化,测试文件单独或换顺序执行时不再依赖其他文件先调用 `init_db()`。
- 本轮不修改数据库 Schema、活动数据、AI Provider、FFmpeg 参数或真实发布流程;没有触发 AI、Chrome、抖音或 B站。
- 最终隔离验收通过:完整测试 `637 passed`;Ruff 和 Python Compileall 全部通过,业务代码与文档的 whitespace 检查通过。Codemap 复评为 `Task Review & Cut 78/B`、`Pipeline & Job Queue 72/C`、`API & Runtime 48/D`、`SQLite Persistence 64/C`。
- API/Persistence 分数下降不是本轮状态修复回归:独立复评删除了已修的状态跳跃/deleted 发现,同时把原始 Secret 读取、可选鉴权、同步重任务阻塞 async 路由、唯一索引重建失败静默吞掉和无版本迁移重新按 HIGH/MED 计分;这些保留到 P1.3b~P1.5。

## 2026-08-24 稳定 V1 P1B.2 发布执行代际与 Worker 幂等

- Scheduler 每次领取发布任务都会保留唯一 `execution_id`;平台结果、阶段、成功、导出、失败、人工复核和安全重排队全部要求命中当前 `PUBLISHING + execution_id`。旧执行写回会返回 `skipped`,同一事务中的结果和事件一并回滚。
Expand Down
7 changes: 5 additions & 2 deletions NEXT_STEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
- [x] P1.1:收紧媒体读取与任务目录边界,补齐核心读写路径的 traversal / arbitrary-file 回归测试;并补充同名目录原子预占和进程树退出确认。
- [x] P1.2a:Workflow Job 增加每次 claim 唯一的 `lease_token`,旧 Worker 的心跳、进度、checkpoint、终态和 release 均不能覆盖新执行。
- [x] P1.2b:Publish Job 使用现有 `execution_id` 完成所有写回 fencing,并让 Windows Publish Worker 对重复 execution 幂等;跨进程锁、恢复 fail-closed 和 Scheduler 优雅停机已覆盖。
- [ ] P1.3:补齐任务状态转移约束、批处理原子性、取消/重启恢复和明确失败状态。
- [x] P1.3a:任务状态接口受控转换;Task/Job lease 代际一致;自动流水线取消恢复、READY 终态和关联发布清理明确;切片批次编号、输出目录、Task 终态和数据库激活原子化。
- [ ] P1.3b:为自动流水线增加按步骤持久化 checkpoint 和副作用复用,补齐进程重启后的精确续跑;再处理字幕批准批次原子性与中断目录清理。
- [ ] P1.3c:修复数据库唯一索引重建失败被静默吞掉的问题,并为幂等迁移增加可追踪版本与失败证据;不在本轮直接迁移活动库。
- [ ] P1.4:统一第三方 AI/FFmpeg 超时、错误 JSON、429/5xx 与重试幂等边界,并避免重复计费。
- [ ] P1.5:在不扩大个人本地项目范围的前提下处理密钥日志、输入校验和本地管理员接口门禁。
- [ ] P2:拆分 God Service、去除查询重复、补核心集成/故障测试和可观测性;不做全面重构。
Expand All @@ -20,7 +22,8 @@
2. 不需要点击“立即发送”;本轮没有执行真实投稿,也没有改变 28 条 `NEED_REVIEW` 的人工确认边界。
3. 若未来永久删除返回 `cleanup_pending`,不要手工移动隔离目录;保留返回信息和 manifest,使用后续安全清理入口重试。
4. 修复前数据库备份位于 `data/backups/workflow-before-foreign-key-repair-20260824-130321-535723-35a0f972.sqlite3`,只有活动库无法通过完整性检查时才考虑恢复,不要直接覆盖当前数据库。
5. P1B.1/P1B.2 代码与隔离测试均已完成;活动库尚未增加 `workflow_jobs.lease_token`。下一独立轮先确认没有活动 Workflow Job,再做在线备份、幂等迁移和正式重启烟测。
5. P1B.1/P1B.2 与 P1.3a 代码、隔离测试均已完成;活动库尚未增加 `workflow_jobs.lease_token`,本轮也未重启正式服务。下一独立轮先确认没有活动 Workflow Job,再做在线备份、幂等迁移和正式重启烟测。
6. P1.3a 不需要手工修改数据库。合并后可用一条低风险测试任务验证:取消后显示“已取消”;正常完成后停在“待人工确认发布”;不要点击真实发布。

## 2026-08-24 工程体检确认的原始整改顺序

Expand Down
17 changes: 17 additions & 0 deletions PROJECT_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ P1B.1 与 P1B.2 已把“旧 Worker/旧发布 execution 覆盖新执行”和“

Codemap 独立复评结果:`Publish Scheduler 62→71(C)`、`Publishers & Worker 62→76(B)`;`Publish Center` 因 4750 行 God Service、原始 Secret 响应和旧 API Provider 风险仍维持 `52(D)`。复评未发现 Scheduler/Worker 新的 HIGH 问题;Worker 尚保留“升级前无 identity/损坏 journal 无法归属 job”的兼容边界,不能据此对历史不确定任务自动重投。

## 0.4 P1.3a 任务状态与切片原子性状态(2026-08-24)

| P1.3a 项目 | 结果 | 验证证据 |
| --- | --- | --- |
| 对外任务状态跳跃 | 已封口 | `PATCH /status` 使用显式允许转换表和同事务条件更新;空任务直接标记完成返回 409,已删除任务拒绝任何后续状态写入。 |
| 取消后的明确终态 | 已封口 | 自动 Job 请求取消时任务进入 `CANCELLED`;流水线在步骤前、步骤后和 READY 前复核取消,不再把取消映射为失败或继续完成。 |
| Task/Job 执行代际 | 已封口 | Task 步骤、切片提交和 READY 写回均校验当前 owner、token、未过期 lease 与取消标记;锁等待跨过租约时旧执行写回被拒。 |
| 取消恢复与发布清理 | 已封口 | 公开取消会同步停止活跃自动 Job;子进程退出或租约过期可收敛为 cancelled;只清理本 workflow_job_id 关联且尚未发布的任务。 |
| `READY_TO_PUBLISH` 瞬态 | 已封口 | 流水线成功后稳定保留 `READY_TO_PUBLISH`,Job 可以完成,但任务继续表示“等待人工确认发布”。 |
| 切片半提交 | 已封口 | 一个批次的全部 output clip、旧 active 停用和新 active 激活在同一 `BEGIN IMMEDIATE` 事务提交;第二条插入失败时整批回滚。 |
| 切片并发覆盖 | 已封口 | run number 在写锁内分配;每个 run 使用独立输出目录;较老 run 最后完成时不能覆盖任意已完成的较新 run。 |
| Task 终态 CAS | 已封口 | cut_run 创建与 `cutting` 同事务;切片终态只从当前 `cutting` 写入,其他流程刚更新的状态不会被晚到批次覆盖。 |
| 失败证据遮蔽 | 已封口 | summary 写入异常降级为日志和空路径,原始失败/取消状态与错误保持不变。 |
| 隔离回归 | 通过 | 完整测试 `637 passed`,Ruff 与 Python Compileall 通过;未调用真实 AI、FFmpeg、Chrome 或平台投稿。 |

本轮没有增加或迁移数据库列,也没有触碰活动数据、真实 AI 或真实投稿。Codemap 增量复评为 `Task Review & Cut 78/B`、`Pipeline & Job Queue 72/C`、`API & Runtime 48/D`、`SQLite Persistence 64/C`。后两项分数下降不是本轮状态修复回归,而是独立复评把原始 Secret 读取、可选鉴权、同步重任务阻塞 async 路由、唯一索引重建失败静默吞掉和无版本迁移重新按实际风险计分。P1.3a 解决的是“状态与切片提交能否可信”这一层;自动流水线的跨进程 step checkpoint、数据库迁移 fail-open、FFprobe/第三方错误边界和 Secret/本地管理员门禁仍按后续独立轮次处理。因此项目继续维持“可用 V1”,不能在剩余 P1 门禁完成前提前宣称“稳定 V1”。

## 1. Executive Summary

### 结论
Expand Down
110 changes: 110 additions & 0 deletions STABILITY_P1_STATE_ATOMICITY_TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# P1.3 任务状态与切片原子性

## 背景

工程审计确认了四个相互关联的稳定性缺口:任务状态接口可以任意跳转;全自动流水线的取消会被记录成失败或继续完成;`READY_TO_PUBLISH` 只短暂存在;切片结果逐条提交,失败时可能留下半成功数据。

基线证据:隔离测试数据库初始化后,相关 8 个测试文件共 `112 passed`,Ruff 检查通过。

## 目标

1. 对外状态更新只能执行明确允许的相邻或恢复转换,禁止把空任务直接标记完成。
2. 自动流水线在步骤前后都检查取消,取消后任务进入明确、可重试的 `CANCELLED` 状态。
3. 自动流水线成功终态稳定保留为 `READY_TO_PUBLISH`,不再立即覆盖为 `COMPLETED`。
4. 同一切片批次的结果写入、旧版本停用和新版本激活在一个 SQLite 事务内完成。
5. 每个切片批次写入独立目录,避免并发/重试覆盖同名媒体文件。
6. 汇总文件写入失败不得遮蔽原始业务错误或取消原因。

## 影响与成本

| 项目 | 影响 | 成本 | 本轮决策 |
| --- | --- | --- | --- |
| 测试数据库统一初始化 | 高 | 低 | 实施 |
| 状态接口受控转换 | 高 | 中 | 实施 |
| 取消终态与 READY 稳定化 | 高 | 中 | 实施 |
| 切片事务与批次目录隔离 | 高 | 中 | 实施 |
| 汇总写入降级 | 中 | 低 | 实施 |
| 流水线跨进程持久化步骤 checkpoint | 高 | 高 | 留到 P1.3b |
| FFprobe、AI Provider、Secret/Auth | 高 | 中至高 | 按后续独立轮次处理 |

## 状态图

修改前:

```text
任意状态 --PATCH /status--> 任意状态
PUBLISH_JOB_CREATING -> READY_TO_PUBLISH -> COMPLETED
运行中 --取消/异常--> FAILED_<STEP> 或残留运行态
```

修改后:

```text
对外 PATCH: 当前状态 --允许表--> 相邻状态/明确恢复状态
PUBLISH_JOB_CREATING -> READY_TO_PUBLISH(稳定终态,等待人工发布)
运行中 --取消--> CANCELLED --重试--> 对应流水线起点
```

## 允许修改范围

- `app/models/task.py`
- `app/routers/tasks.py`
- `app/services/task_lifecycle_service.py`
- `app/services/task_service.py`
- `app/services/pipeline_engine.py`
- `app/services/auto_publish_service.py`
- `app/services/job_service.py`
- `app/services/job_worker.py`
- `app/services/video_cut_workflow_service.py`
- 与上述行为直接相关的测试
- `PROJECT_AUDIT.md`、`DEVELOPMENT_LOG.md`、`NEXT_STEPS.md`
- Codemap 生成数据与产物(必须通过 skill 脚本生成)

## 禁止修改范围

- 数据库 Schema 和生产数据
- AI Provider、Prompt、FFmpeg 参数和发布平台逻辑
- UI 结构与样式
- 依赖版本
- Dead Code 删除和大规模模块拆分

## 已确定实现要求

- 保留手动流程小写状态与自动流程大写状态的兼容性。
- 内部工作流仍可写步骤状态;对外 `PATCH /status` 必须走独立的合法转换检查和数据库条件更新。
- 所有任务状态写入拒绝已永久删除任务。
- 切片数据库提交失败时,新批次标记失败,旧 active 版本保持不变。
- 较旧并发批次不得覆盖已经完成的较新 active 批次。
- 发布中心同步只能在当前批次成功激活后执行。
- 取消检查放在步骤开始前和处理完成后,最终 READY 写入前再检查一次。
- Task 步骤状态、切片提交和 READY 写回必须绑定当前 Workflow Job 的 owner、token 与未过期 lease。
- 公开 Task 取消必须同步请求活跃自动 Job 停止;Job 取消只清理 provider 证据明确关联到本执行代际的未发布任务。
- `running + cancel_requested` 在子进程退出或 lease 过期后必须收敛到 `cancelled`,不能永久残留运行态。

## 验收标准

- 空任务不能通过 API 直接变成 `completed`/`COMPLETED`,返回 HTTP 409。
- 合法相邻状态转换成功;并发状态变化时条件更新失败,不覆盖新状态。
- 取消的自动任务最终为 `CANCELLED`,job 为 `cancelled`,不会写 READY/COMPLETED。
- 旧 lease Worker 不能写 Task 步骤、切片结果或 READY;READY 后取消会清理本轮关联且尚未发布的排期记录。
- 成功流水线最终任务状态和返回状态均为 `READY_TO_PUBLISH`/`ready_to_publish`。
- 人为制造第二条 output clip 插入失败时,数据库中不保留该批次的部分 clip,旧 active 结果不变。
- 并发创建 cut run 时 `run_number` 不重复;批次输出目录不同。
- 汇总写入失败时仍返回原始失败/取消结果。
- 定向测试、全量测试和 Ruff 全部通过。

## 建议测试命令

```powershell
python -m pytest tests/test_task_state_machine.py tests/test_cut_atomicity.py tests/test_auto_pipeline.py tests/test_job_queue.py tests/test_job_fencing.py -q
python -m pytest -q
python -m ruff check app tests
```

## 回滚方式

本轮不迁移 Schema;回滚代码提交即可。新生成的 `clips/run_*` 目录由现有数据库路径引用,旧目录和旧记录不会删除。`provider_response.workflow_job_id` 只是本地关联证据,不改变发布表结构。

## 返回格式

报告修改文件、状态行为变化、测试证据、Codemap 复审结果、提交哈希、分支、Push 与 PR 链接。
2 changes: 1 addition & 1 deletion app/db/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ def _migrate_tasks_table(connection: sqlite3.Connection) -> None:
'CLIP_SELECTING', 'VIDEO_CUTTING', 'SUBTITLE_DRAFTING',
'PENDING_SUBTITLE_REVIEW', 'METADATA_GENERATING',
'SCHEDULE_CREATING', 'PUBLISH_JOB_CREATING', 'READY_TO_PUBLISH',
'COMPLETED', 'FAILED_PREPARING_SOURCE', 'FAILED_TRANSCRIBING',
'COMPLETED', 'CANCELLED', 'FAILED_PREPARING_SOURCE', 'FAILED_TRANSCRIBING',
'FAILED_AI_ANALYZING', 'FAILED_CLIP_SELECTING', 'FAILED_VIDEO_CUTTING',
'FAILED_SUBTITLE_DRAFTING',
'FAILED_METADATA_GENERATING', 'FAILED_SCHEDULE_CREATING',
Expand Down
1 change: 1 addition & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class TaskStatus(str, Enum):
PUBLISH_JOB_CREATING = "PUBLISH_JOB_CREATING"
READY_TO_PUBLISH = "READY_TO_PUBLISH"
COMPLETED = "COMPLETED"
CANCELLED = "CANCELLED"
FAILED_PREPARING_SOURCE = "FAILED_PREPARING_SOURCE"
FAILED_TRANSCRIBING = "FAILED_TRANSCRIBING"
FAILED_AI_ANALYZING = "FAILED_AI_ANALYZING"
Expand Down
5 changes: 4 additions & 1 deletion app/routers/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ async def delete_task(task_id: str) -> dict:

@router.patch("/{task_id}/status")
async def patch_task_status(task_id: str, payload: TaskStatusUpdate) -> dict:
task = task_service.update_task_status(task_id, payload.status, payload.error_message)
try:
task = task_service.transition_task_status(task_id, payload.status, payload.error_message)
except task_service.TaskStatusConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
return task
Expand Down
2 changes: 2 additions & 0 deletions app/services/auto_publish_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def create_auto_publish_jobs(
scheduled_items: list[dict],
*,
subtitle_delivery_mode: str,
workflow_job_id: str | None = None,
) -> dict:
"""为全自动流水线生成发布任务。

Expand Down Expand Up @@ -106,6 +107,7 @@ def create_auto_publish_jobs(
job_id = uuid4().hex[:12]
provider_response = {
"source": "auto_pipeline",
"workflow_job_id": workflow_job_id or "",
"target_platform": platform,
"metadata_source": metadata.get("source") or "",
"metadata_error": metadata.get("error") or "",
Expand Down
Loading