Files
meijiaka-zy/docs/semantic-refactoring-plan.md
T

426 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端语义治理与架构重构计划
> **范围**`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 语法 `<<<element_1>>>` 直接出现在 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`
### P2CRUD 层裸字典更新
- **症状**`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 4Scheduler 内部统一叫 `job`
4. `dict[str, Any]` 禁止出现在跨层传递的接口中
---
## 三、重构阶段(Phase 1-5
每个 Phase 独立成组,建议按顺序执行。每个 Phase 完成后必须跑通 `pytest``make lint`
---
### Phase 1Schema 统一 + 状态机 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 2Scheduler 层全面"去 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.2Registry 承担全部序列化职责
- [ ] 在 `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.4API 层适配
- [ ] `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.1API 层删除 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.2DB 模型增加供应商抽象
- [ ] `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.3Scheduler 模型供应商抽象
- [ ] `app/scheduler/models.py`Phase 2 后的 `JobRecord` 及 `Segment`):
- `kling_task_id` → `provider_task_id`
- 如需追溯供应商,增加 `provider: str = "kling"`
#### Task 3.4Provider 返回值模型化
- [ ] 新建 `app/ai/providers/kling_dto.py`
- `KlingVideoResult`
- `KlingImageResult`
- `KlingVoiceResult`
- `KlingElementResult`
- [ ] 修改 `app/ai/providers/klingai_provider.py`
- 所有返回裸 `dict[str, Any]` 的方法改为返回对应的 `Kling*Result`
- 状态字段类型改为 `KlingTaskStatus`
#### Task 3.5Prompt 语法迁移到 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.6Service 层适配器映射
- [ ] `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 "<<<element_1>>>\|<<<voice_1>>>" 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 5CRUD 层强类型化
**目标**:消灭 CRUD 层的裸字典更新
**预估工时**2 天
**影响面**CRUD Base、Avatar CRUD、Scheduler Handler
#### Task 5.1CRUD Base 类型约束
- [ ] `app/crud/base.py`
- `obj_in: dict[str, Any]` → `obj_in: CreateSchemaType | UpdateSchemaType`
- 保留 `dict` 仅作为 `update` 的 fallback,但所有业务调用方优先使用 Schema
#### Task 5.2Avatar 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.3Handler 调用方改造
- [ ] `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 "<<<element_1>>>\|<<<voice_1>>>" 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)(术语表与分层禁令)