# 后端语义治理与架构重构计划 > **范围**:`python-api/app/` 全目录 > **目标**:根治需求调整与 Celery→Async Scheduler 迁移导致的命名腐败、语义漂移、类型漂移问题 > **原则**:长期稳定优先,不计短期开发成本 > **状态**:计划中(待团队评审后执行) --- ## 一、问题诊断 经过深度代码扫描,当前代码存在六大类语义腐败,按严重程度排序: ### P0:调度器核心模型的 `params: dict[str, Any]` 类型漂移 - **症状**:所有 Handler 从裸字典里 `params.get("shots")`,且因 Redis 序列化反复写 `if isinstance(shots, str): shots = json.loads(shots)` - **风险**:这是 Scheduler 取代 Celery 后最脆弱的环节,任何字段名改动都会导致运行时崩溃 - **关键文件**:`app/scheduler/models.py`, `app/scheduler/registry.py`, `app/scheduler/handlers/video_handler.py` ### P0:`shot/segment/scene` 的三重命名 + 四处重复定义 - **症状**:`ScriptShot`(Schema)、`ShotData`(API)、`ShotTask`(Service)、`ShotUnit`(Scheduler)四者并存,字段名 `scene`/`scene_desc`、`type`/`shot_type` 混用 - **风险**:任何分镜字段改动必须改 4 个类,极易遗漏 - **关键文件**:`app/schemas/script.py`, `app/api/v1/video.py`, `app/services/kling_video_service.py`, `app/scheduler/models.py` ### P0:Kling 供应商语义大规模泄漏到业务层和 API 层 - **症状**:`element_id`(Kling 主体 ID)、`kling_task_id`、Omni prompt 语法 `<<>>` 直接出现在 API Schema、Scheduler 模型、数据库模型中 - **风险**:一旦更换视频供应商,影响面会穿透所有层级 - **关键文件**:`app/api/v1/video.py`, `app/api/v1/tasks.py`, `app/models/avatar.py`, `app/scheduler/handlers/video_handler.py` ### P1:`task` / `task_id` 的五重语义混用 - **症状**:FastAPI BackgroundTask、Scheduler Task、Kling API Task、AnyToCopy Task、Volcengine Task 都叫 `task` - **风险**:日志堆栈中无法区分层级,调试极其困难 - **关键文件**:`app/api/v1/tasks.py`, `app/scheduler/`, `app/ai/providers/klingai_provider.py`, `app/services/` ### P1:历史残留命名与双轨运行 - **症状**:`# 兼容旧字段`、`video_task_id`、`image_task_id`、`cache_err` 等 Celery 时代残留;`script` 任务仍走 BackgroundTask,其他任务走 Scheduler - **风险**:双轨运行导致统一监控、重试、日志无法覆盖全部任务类型 - **关键文件**:`app/scheduler/handlers/video_handler.py`, `app/scheduler/handlers/image_handler.py`, `app/api/v1/tasks.py` ### P2:CRUD 层裸字典更新 - **症状**:`avatar_crud.update(db, db_obj=avatar, obj_in={"status": "element_pending"})` - **风险**:拼写错误、状态值非法无法被静态检查捕获 - **关键文件**:`app/crud/base.py`, `app/crud/avatar.py`, `app/scheduler/handlers/avatar_handler.py` --- ## 二、架构目标:六层语义治理 我们将整个后端严格划分为 **6 个语义层级**,每一层只使用属于该层的术语: ``` ┌─────────────────────────────────────────────────────────┐ │ Layer 6: Presentation (API Schema / 前端适配层) │ │ 术语: Segment, Human, Job, Script, Cover │ ├─────────────────────────────────────────────────────────┤ │ Layer 5: Application (API 路由 / BackgroundJob) │ │ 术语: Segment, Human, Job, Project │ ├─────────────────────────────────────────────────────────┤ │ Layer 4: Orchestration (Scheduler / SlotManager) │ │ 术语: Job, JobRecord, Slot, Handler │ ├─────────────────────────────────────────────────────────┤ │ Layer 3: Domain (Service / 业务逻辑) │ │ 术语: Segment, Human, VideoComposition, Caption │ ├─────────────────────────────────────────────────────────┤ │ Layer 2: Adapter (Provider Client / 供应商适配) │ │ 术语: KlingJob, KlingElement, VolcJob, ProviderTaskId │ ├─────────────────────────────────────────────────────────┤ │ Layer 1: Infrastructure (DB / Redis / HTTP / FileSys) │ │ 术语: 仅使用底层技术术语 │ └─────────────────────────────────────────────────────────┘ ``` ### 核心禁令 1. `element`、`omni`、`kling_task_id` 等**供应商术语**禁止出现在 Layer 3 以上 2. `shot` 禁止出现在 Layer 3 以上(Kling 术语,业务层统一叫 `segment`) 3. `task` 禁止出现在 Layer 4(Scheduler 内部统一叫 `job`) 4. `dict[str, Any]` 禁止出现在跨层传递的接口中 --- ## 三、重构阶段(Phase 1-5) 每个 Phase 独立成组,建议按顺序执行。每个 Phase 完成后必须跑通 `pytest` 和 `make lint`。 --- ### Phase 1:Schema 统一 + 状态机 Enum 化 **目标**:建立"单一真相源",消除 shot/segment/scene 的四重定义 **预估工时**:3-4 天 **影响面**:全项目基础类型 #### Task 1.1:新建统一术语 Schema - [ ] 新建 `app/schemas/segment.py` ```python class Segment(BaseModel): id: str type: Literal["segment", "empty_shot"] scene: str # 统一为 scene,删除 scene_desc voiceover: str duration: int | None = None human_id: str | None = None # 业务术语,对应 Kling 的 element_id status: SegmentStatus = SegmentStatus.PENDING provider_task_id: str | None = None video_url: str | None = None local_path: str | None = None query_fail_count: int = 0 ``` - [ ] 新建 `app/schemas/enums.py`,定义以下 Enum: - `JobStatus`: pending, running, completed, failed - `SegmentStatus`: pending, submitted, completed, failed - `AvatarCloneStatus`: pending, voice_processing, voice_failed, element_pending, element_processing, element_failed, succeed - `KlingTaskStatus`: submitted, processing, succeed, failed(仅限 Provider 层使用) - [ ] 新建 `app/schemas/job.py`,定义 `JobParams` Union: - `VideoJobParams`(含 `segments: list[Segment]`) - `ImageJobParams` - `SubtitleJobParams` - `CopyJobParams` - `AvatarCloneJobParams` #### Task 1.2:删除重复定义 - [ ] 删除 `app/scheduler/models.py` 中的 `ShotUnit` - [ ] 删除/重构 `app/services/kling_video_service.py` 中的 `ShotTask`(字段迁移到 `Segment`) - [ ] 删除 `app/api/v1/video.py` 中的 `ShotData`,改为引用 `Segment` - [ ] 将 `app/schemas/script.py` 中的 `ScriptShot` 改为 `Segment` 的别名或类型适配器 #### Task 1.3:字段名统一 - [ ] 批量将 `scene_desc` 重命名为 `scene`(覆盖 `kling_video_service.py`, `video_handler.py` 等) - [ ] 批量将 `shot_type` 重命名为 `type`(在业务层/Schema 层;Provider 层保留 `shot_type` 仅用于 Kling API 调用) - [ ] `app/api/v1/tasks.py` 中的 `shots: list[dict]` 改为 `segments: list[Segment]` #### Task 1.4:状态字符串 Enum 化 - [ ] `app/scheduler/models.py` 中 `TaskRecord.status` 类型改为 `JobStatus` - [ ] `app/services/kling_video_service.py` 中 `VideoGenerationJob.status` 类型改为 `JobStatus` - [ ] `app/models/avatar.py` 中 `Avatar.status` 类型改为 `AvatarCloneStatus` - [ ] `app/ai/providers/klingai_provider.py` 中所有 Kling 状态字符串操作改为 `KlingTaskStatus` #### 验收标准 - [ ] `grep -rn "class ShotUnit\|class ShotTask\|class ShotData\|class ScriptShot" app/` 返回为空(除了注释或别名声明) - [ ] `grep -rn "scene_desc" app/ | grep -v "__pycache__"` 返回为空 - [ ] `mypy app/schemas/` 无类型错误 - [ ] `pytest` 通过 --- ### Phase 2:Scheduler 层全面"去 task 化" **目标**:消除 `task` 的五重语义混用,建立 `Job` 专属语义域 **预估工时**:3-4 天 **影响面**:`app/scheduler/` 目录及引用方 #### Task 2.1:核心模型与 Registry 重命名 - [ ] `app/scheduler/models.py`:`TaskRecord` → `JobRecord` - [ ] `app/scheduler/registry.py`:`TaskRegistry` → `JobRegistry` - 所有 `task_id` 参数/字段 → `job_id` - 所有 `task_type` 参数/字段 → `job_type` - `running_task_ids` → `running_job_ids` - [ ] `app/scheduler/engine.py`:`AsyncEngine` 中所有 `task` → `job` #### Task 2.2:Registry 承担全部序列化职责 - [ ] 在 `JobRegistry.get()` 中统一完成 JSON 反序列化 - 解析 `params` 字段时,根据 `job_type` 路由到正确的 `JobParams` Pydantic 模型 - 保证 Handler 拿到的 `job.params` 永远是强类型对象 - [ ] 删除 `video_handler.py` 和 `image_handler.py` 中所有的 `isinstance(shots, str): json.loads` 逻辑 - [ ] 删除 `_download_and_upload` 中的手动 JSON 类型判断 #### Task 2.3:`StateChange` 取代裸字典 - [ ] 在 `app/scheduler/models.py` 中定义: ```python @dataclass(frozen=True) class StateChange: job_id: str field: str value: Any ``` - [ ] 修改 `app/scheduler/engine.py`: - `_apply_changes(self, changes: list[dict[str, Any]])` → `_apply_changes(self, changes: list[StateChange])` - 序列化逻辑移入 `StateChange.to_redis_command()` 方法 - [ ] 修改 `app/scheduler/handlers/base.py`:`tick()` 返回类型改为 `list[StateChange]` - [ ] 修改所有 Handler:`changes.append({"task_id": ..., "field": ...})` → `changes.append(StateChange(job_id=..., field=..., value=...))` #### Task 2.4:API 层适配 - [ ] `app/api/v1/tasks.py` 中:内部变量名 `task_id` 在引用 Scheduler 时改为 `job_id`(API URL `/tasks/{task_id}` 保持不变,仅内部变量和注释调整) - [ ] `app/api/v1/avatar.py` 中:引用 `TaskRegistry` 的地方改为 `JobRegistry` #### 验收标准 - [ ] `grep -rn "TaskRecord\|TaskRegistry\|running_task_ids" app/scheduler/` 返回为空 - [ ] `grep -rn "isinstance(.*shots.*str)" app/scheduler/handlers/` 返回为空 - [ ] `grep -rn '"task_id":' app/scheduler/handlers/` 返回为空(仅 `StateChange` dataclass 内部可保留) - [ ] `pytest` 通过 --- ### Phase 3:建立"供应商防火墙"(Adapter 层隔离) **目标**:将 Kling/Volc 术语彻底下压到 Provider 层,业务层以上使用纯业务语义 **预估工时**:4-5 天 **影响面**:API Schema、DB 模型、Scheduler 模型、Provider 层 #### Task 3.1:API 层删除 Kling 术语泄漏 - [ ] `app/api/v1/video.py`: - `element_id: int | None = Field(None, description="Kling主体ID")` → `human_id: int | None = Field(None, description="数字人主体ID")` - [ ] `app/api/v1/tasks.py`: - 同上,所有 `element_id` → `human_id` - `VideoParams` 中的 `shots` → `segments` #### Task 3.2:DB 模型增加供应商抽象 - [ ] `app/models/avatar.py`: - `element_id: Mapped[int | None]` → `provider_element_id: Mapped[int | None]` - `voice_task_id` → `provider_voice_job_id` - `element_task_id` → `provider_element_job_id` - 新增 `provider: Mapped[str] = mapped_column(default="kling")`(为未来多供应商做准备) - [ ] 生成 Alembic 迁移脚本(字段重命名 + 新增字段) #### Task 3.3:Scheduler 模型供应商抽象 - [ ] `app/scheduler/models.py`(Phase 2 后的 `JobRecord` 及 `Segment`): - `kling_task_id` → `provider_task_id` - 如需追溯供应商,增加 `provider: str = "kling"` #### Task 3.4:Provider 返回值模型化 - [ ] 新建 `app/ai/providers/kling_dto.py`: - `KlingVideoResult` - `KlingImageResult` - `KlingVoiceResult` - `KlingElementResult` - [ ] 修改 `app/ai/providers/klingai_provider.py`: - 所有返回裸 `dict[str, Any]` 的方法改为返回对应的 `Kling*Result` - 状态字段类型改为 `KlingTaskStatus` #### Task 3.5:Prompt 语法迁移到 Provider 层 - [ ] 删除 `app/scheduler/handlers/video_handler.py` 中的: - `_build_omni_prompt()` - `_build_empty_shot_video_prompt()` - [ ] 在 `app/ai/providers/klingai_provider.py` 中新建 `KlingPromptBuilder` 类: ```python class KlingPromptBuilder: @staticmethod def omni_segment(scene: str, voiceover: str, element_id: str | None = None) -> str @staticmethod def empty_shot(scene: str, voiceover: str) -> str ``` - [ ] `video_handler.py` 调用时只传业务字段(`scene`, `voiceover`, `human_id`),由 Provider 层负责映射为 Kling 语法 #### Task 3.6:Service 层适配器映射 - [ ] `app/services/kling_video_service.py`: - 删除 `avatar_id` 废弃字段 - `human_id` → 在调用 Provider 时映射为 `element_id` - [ ] `app/services/qiniu_service.py`: - `file_type="avatar"` → `file_type="clone_video"` 或 `"human_video"` #### 验收标准 - [ ] `grep -rn "element_id" app/api/ app/schemas/ app/scheduler/models.py | grep -v "provider_element_id"` 返回为空 - [ ] `grep -rn "kling_task_id" app/api/ app/schemas/ app/scheduler/models.py` 返回为空 - [ ] `grep -rn "<<>>\|<<>>" app/scheduler/ app/services/` 返回为空(仅在 Provider 层保留) - [ ] Alembic 迁移脚本可正常升级/降级 - [ ] `pytest` 通过 --- ### Phase 4:清理历史残留 + 消除双轨运行 **目标**:删除所有 Celery 时代残留,将 `script` 任务纳入 Scheduler 统一调度 **预估工时**:2-3 天 **影响面**:Handler、API 路由、历史命名 #### Task 4.1:删除兼容旧字段代码 - [ ] `app/scheduler/handlers/video_handler.py`: - 删除 `shot["video_task_id"] = kling_task_id # 兼容旧字段` - 删除初始化 shots 时的 `"video_task_id": None` - [ ] `app/scheduler/handlers/image_handler.py`: - 删除 `params["image_task_id"] = kling_task_id` - [ ] `app/services/kling_video_service.py`: - 删除 `avatar_id` 字段 #### Task 4.2:修正历史残留命名 - [ ] `app/core/redis_client.py`:删除文档字符串中的 `RateLimiter` 字样 - [ ] `app/api/v1/tasks.py`: - `cache entry` → `registry entry` - `cache_err` → `registry_err` - `Failed to update cache` → `Failed to update registry` - [ ] `app/core/token_manager.py`:`_background_tasks` → `_refresh_tasks` - [ ] 删除 `app/services/dto.py` #### Task 4.3:将 `script` 任务迁移到 Scheduler - [ ] 新建 `app/scheduler/handlers/script_handler.py` - 将 `app/api/v1/tasks.py` 中 `_run_script_task` 的逻辑迁移至此 - 继承 `AsyncHandler`,`name = "script"`,不占用 Slot(或占用独立 `script_slots`) - [ ] 修改 `app/api/v1/tasks.py`: - `script` 任务改为 `registry.create(job_type="script", ...)` - 删除 `BackgroundTasks` 相关参数和 `_run_script_task` 函数 - [ ] 修改 `app/scheduler/main.py`:注册 `ScriptHandler` #### 验收标准 - [ ] `grep -rn "兼容旧字段\|video_task_id\|image_task_id" app/scheduler/ app/services/` 返回为空 - [ ] `grep -rn "cache_err\|cache entry" app/api/v1/tasks.py` 返回为空 - [ ] `app/services/dto.py` 不存在 - [ ] `app/api/v1/tasks.py` 中无 `BackgroundTasks` 导入和使用 - [ ] `pytest` 通过 --- ### Phase 5:CRUD 层强类型化 **目标**:消灭 CRUD 层的裸字典更新 **预估工时**:2 天 **影响面**:CRUD Base、Avatar CRUD、Scheduler Handler #### Task 5.1:CRUD Base 类型约束 - [ ] `app/crud/base.py`: - `obj_in: dict[str, Any]` → `obj_in: CreateSchemaType | UpdateSchemaType` - 保留 `dict` 仅作为 `update` 的 fallback,但所有业务调用方优先使用 Schema #### Task 5.2:Avatar Schema 定义 - [ ] 新建 `app/schemas/avatar.py`: ```python class AvatarCreate(BaseModel): name: str video_url: str status: AvatarCloneStatus = AvatarCloneStatus.PENDING class AvatarUpdate(BaseModel): name: str | None = None status: AvatarCloneStatus | None = None provider_voice_job_id: str | None = None provider_element_job_id: str | None = None provider_element_id: int | None = None fail_reason: str | None = None ``` - [ ] `app/crud/avatar.py`:改为 `class CRUDAvatar(CRUDBase[Avatar, AvatarCreate, AvatarUpdate])` #### Task 5.3:Handler 调用方改造 - [ ] `app/scheduler/handlers/avatar_handler.py`: - 所有 `_update_avatar(avatar_id, {"status": "..."})` 改为 `_update_avatar(avatar_id, AvatarUpdate(status=AvatarCloneStatus.XXX))` - 删除裸字典辅助函数 `_update_avatar` 中的 `**obj_in` 展开,改用 `obj_in.model_dump(exclude_unset=True)` #### 验收标准 - [ ] `grep -rn 'obj_in=\{' app/scheduler/handlers/avatar_handler.py` 返回为空 - [ ] `mypy app/crud/` 无类型错误 - [ ] `pytest` 通过 --- ## 四、自动化防护网(Phase 5 之后部署) ### 4.1 预提交钩子:禁词检查 在 `.pre-commit-config.yaml` 或 `Makefile` 中增加 `lint-semantic`: ```makefile lint-semantic: @echo "Checking semantic boundaries..." @! grep -rn "element_id" app/api/ app/schemas/ app/scheduler/models.py | grep -v "provider_element_id" || (echo "ERROR: element_id leaked to upper layers"; exit 1) @! grep -rn "kling_task_id" app/api/ app/schemas/ app/scheduler/models.py || (echo "ERROR: kling_task_id leaked to upper layers"; exit 1) @! grep -rn "scene_desc" app/ | grep -v "__pycache__" || (echo "ERROR: scene_desc not fully renamed"; exit 1) @! grep -rn "TaskRecord\|TaskRegistry\|running_task_ids" app/scheduler/ || (echo "ERROR: Scheduler task naming not fully migrated"; exit 1) @! grep -rn "<<>>\|<<>>" app/scheduler/ app/services/ || (echo "ERROR: Kling prompt syntax leaked"; exit 1) @echo "Semantic check passed" ``` ### 4.2 mypy 渐进严格化 - [ ] 在 `pyproject.toml` 中为 `app/scheduler/` 和 `app/schemas/` 单独开启 `strict = true` - [ ] 逐步扩展至 `app/api/` 和 `app/services/` ### 4.3 AGENTS.md 术语表(Glossary) 在 `AGENTS.md` 中新增"统一术语表"章节(见下文),所有 AI Agent 修改代码前必须查阅。 --- ## 五、风险与回滚策略 | 风险 | 影响 | mitigation | |------|------|-------------| | Phase 1 删除 `ScriptShot` 影响前端类型生成 | 中 | `ScriptShot` 保留为 `Segment` 的 `TypeAlias` 一个 Sprint,待前端适配后删除 | | Phase 2 `JobRegistry` 重命名导致 API 层引用遗漏 | 高 | 使用 IDE 全局重构(PyCharm/Ruff/Rename Symbol),执行后跑全量 `pytest` | | Phase 3 DB 字段重命名需要数据迁移 | 中 | Alembic 脚本必须包含 `op.alter_column` 的 `existing_type` 和 `existing_nullable` | | Phase 4 `script` 迁出 BackgroundTask 后响应时间变长 | 低 | 脚本生成仍立即返回 `job_id`,前端通过轮询 `/tasks/{job_id}` 获取结果,接口契约不变 | | 多 Phase 并行开发导致冲突 | 高 | **严禁并行**:必须按 1→2→3→4→5 顺序执行,每个 Phase 合并到主分支后再开始下一个 | --- ## 六、作为 GitHub Project Task List 的格式 若导入 GitHub Project,建议按以下结构建 5 个 Milestone: ```markdown ### Milestone 1: Schema Unification - [ ] #101 Create `app/schemas/segment.py` with `Segment` model - [ ] #102 Create `app/schemas/enums.py` with `JobStatus`, `SegmentStatus`, `AvatarCloneStatus`, `KlingTaskStatus` - [ ] #103 Create `app/schemas/job.py` with `JobParams` Union - [ ] #104 Remove `ShotUnit` from `app/scheduler/models.py` - [ ] #105 Remove `ShotTask` from `app/services/kling_video_service.py` - [ ] #106 Remove `ShotData` from `app/api/v1/video.py` - [ ] #107 Rename `scene_desc` → `scene` across codebase - [ ] #108 Migrate all `status` strings to Enums ### Milestone 2: Scheduler De-tasking - [ ] #201 Rename `TaskRecord` → `JobRecord` - [ ] #202 Rename `TaskRegistry` → `JobRegistry` - [ ] #203 Registry auto-deserializes `JobParams` models - [ ] #204 Replace `dict` changes with `StateChange` dataclass - [ ] #205 Update all Handlers to return `list[StateChange]` ### Milestone 3: Vendor Firewall - [ ] #301 API layer: `element_id` → `human_id` - [ ] #302 DB model: add `provider` field, rename task/element IDs - [ ] #303 Scheduler model: `kling_task_id` → `provider_task_id` - [ ] #304 Provider DTOs: `KlingVideoResult`, `KlingImageResult`, etc. - [ ] #305 Move `KlingPromptBuilder` to Provider layer - [ ] #306 Alembic migration for avatar table changes ### Milestone 4: Cleanup & Unification - [ ] #401 Remove legacy compatibility fields (`video_task_id`, `image_task_id`) - [ ] #402 Fix historical naming (`cache_err`, `RateLimiter` docstrings, etc.) - [ ] #403 Delete `app/services/dto.py` - [ ] #404 Migrate `script` task from BackgroundTask to Scheduler ### Milestone 5: CRUD Strong Typing - [ ] #501 Create `AvatarCreate` / `AvatarUpdate` schemas - [ ] #502 Type-constrain CRUDBase - [ ] #503 Refactor `avatar_handler.py` to use `AvatarUpdate` instead of raw dicts - [ ] #504 Add `lint-semantic` to Makefile / pre-commit - [ ] #505 Update `AGENTS.md` with Glossary and layer rules ``` --- ## 七、相关文档 - [统一异步调度器设计文档](./unified-async-scheduler.md) - [数据库设计文档](./database-design.md) - [AGENTS.md](../AGENTS.md)(术语表与分层禁令)