diff --git a/.codebuddy/plans/simple-refactoring-plan.md b/.codebuddy/plans/simple-refactoring-plan.md new file mode 100644 index 00000000..9dee48e1 --- /dev/null +++ b/.codebuddy/plans/simple-refactoring-plan.md @@ -0,0 +1,670 @@ +# 简化重构方案:真正降低复杂度 + +## 执行摘要 + +**目标**: 解决类型重复、服务分散等实际问题,同时减少代码量 +**预期结果**: 删除 3-5 个文件,减少 ~500 行代码 +**时间估计**: 1-2 天 +**风险**: 低 - 每步独立可测试,可随时回滚 + +## 当前问题分析 + +### 问题 1: 类型定义重复(高优先级) + +**现状**: +- `packages/drawnix/src/domain/task/` - 新 DDD 层类型(7 个文件) +- `packages/drawnix/src/types/task.types.ts` - 旧类型(已改为 re-export) +- `apps/web/src/sw/task-queue/types.ts` - SW 端重复定义 +- `packages/drawnix/src/services/sw-channel/types/task.ts` - 通信层类型 + +**问题**: +- 4 处定义,依赖注释 "Keep in sync" 维护一致性 +- DDD 重构后变成 3 种表示:interface、class、snapshot +- 转换代码散落各处 + +### 问题 2: Workflow 服务职责分散(中优先级) + +**现状**: +- `workflowSubmissionService` - 提交工作流 +- `workflowPollingService` - 轮询状态 +- `workflowCompletionService` - 完成后处理 +- 3 个服务管理同一个工作流生命周期 + +**问题**: +- 状态分散在 3 个服务中 +- 服务间通过方法调用耦合 +- 难以追踪完整生命周期 + +### 问题 3: 事件类型不统一(低优先级) + +**现状**: +- 各服务使用独立 RxJS Subject +- 事件结构不一致 +- 缺少类型约束 + +**问题**: +- 订阅者需要知道具体 Subject 位置 +- 事件数据结构不统一 +- 难以调试事件流 + +### 问题 4: DDD 重构增加的复杂度(需回滚) + +**现状**: +- 新增 30 个文件(domain/application/infrastructure) +- Task 有 3 种表示(interface/class/snapshot) +- Repository 包装现有 storage +- Strategy 包装现有条件判断 + +**问题**: +- 代码量增加 3000+ 行 +- 旧代码保留"向后兼容",实际是双重维护 +- 抽象层没有解决实际问题 + +## 解决方案 + +### 方案 1: 统一类型定义(解决问题 1) + +#### 目标 +- 所有类型定义在一个位置 +- SW 和主线程都从同一源导入 +- 删除重复定义 + +#### 实施步骤 + +**Step 1.1: 创建共享类型文件** + +创建 `packages/drawnix/src/types/shared/core.types.ts`: + +```typescript +/** + * 核心类型定义 - 主线程和 SW 共享 + * + * 这是任务和工作流类型的唯一真相来源。 + * 主线程和 SW 都从这里导入,确保类型一致性。 + */ + +// ============================================================================ +// Task 核心类型 +// ============================================================================ + +export enum TaskStatus { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', +} + +export enum TaskType { + IMAGE = 'image', + VIDEO = 'video', + CHARACTER = 'character', + INSPIRATION_BOARD = 'inspiration_board', + CHAT = 'chat', +} + +export enum TaskExecutionPhase { + SUBMITTING = 'submitting', + POLLING = 'polling', + DOWNLOADING = 'downloading', +} + +export interface GenerationParams { + prompt: string; + width?: number; + height?: number; + size?: string; + duration?: number; + style?: string; + model?: string; + seed?: number; + sourceVideoTaskId?: string; + characterTimestamps?: string; + sourceLocalTaskId?: string; + gridImageRows?: number; + gridImageCols?: number; + gridImageLayoutStyle?: 'scattered' | 'grid' | 'circular'; + inspirationBoardLayoutStyle?: 'inspiration-board'; + isInspirationBoard?: boolean; + inspirationBoardImageCount?: number; + autoInsertToCanvas?: boolean; + aspectRatio?: string; + [key: string]: unknown; +} + +export interface TaskResult { + url: string; + width?: number; + height?: number; + duration?: number; + thumbnailUrl?: string; + metadata?: Record; +} + +export interface TaskError { + code: string; + message: string; + details?: unknown; +} + +export interface Task { + id: string; + type: TaskType; + status: TaskStatus; + params: GenerationParams; + createdAt: number; + updatedAt: number; + startedAt?: number; + completedAt?: number; + result?: TaskResult; + error?: TaskError; + userId?: string; + progress?: number; + remoteId?: string; + executionPhase?: TaskExecutionPhase; + savedToLibrary?: boolean; + insertedToCanvas?: boolean; +} + +// ============================================================================ +// 工具函数 +// ============================================================================ + +export const TERMINAL_STATUSES: TaskStatus[] = [ + TaskStatus.COMPLETED, + TaskStatus.FAILED, + TaskStatus.CANCELLED, +]; + +export const RETRYABLE_STATUSES: TaskStatus[] = [ + TaskStatus.FAILED, + TaskStatus.CANCELLED, +]; + +export function isTerminalStatus(status: TaskStatus): boolean { + return TERMINAL_STATUSES.includes(status); +} + +export function isRetryableStatus(status: TaskStatus): boolean { + return RETRYABLE_STATUSES.includes(status); +} +``` + +**Step 1.2: 更新 SW 端导入** + +修改 `apps/web/src/sw/task-queue/types.ts`: + +```typescript +/** + * Service Worker Task Queue Type Definitions + * + * 从共享类型导入,确保与主线程一致。 + */ + +// 从共享类型导入核心定义 +export { + TaskStatus, + TaskType, + TaskExecutionPhase, + GenerationParams, + TaskResult, + TaskError, + Task, + TERMINAL_STATUSES, + RETRYABLE_STATUSES, + isTerminalStatus, + isRetryableStatus, +} from '../../../../packages/drawnix/src/types/shared/core.types'; + +// SW 特有的类型定义保留在这里 +export interface SWTaskQueueConfig { + // ... +} +``` + +**Step 1.3: 删除 domain 层** + +删除整个 `packages/drawnix/src/domain/` 目录(18 个文件): +- `domain/task/` (7 文件) +- `domain/workflow/` (5 文件) +- `domain/events/` (5 文件) +- `domain/index.ts` (1 文件) + +**Step 1.4: 删除 application 层** + +删除 `packages/drawnix/src/application/` 目录(3 个文件): +- `application/task-application-service.ts` +- `application/workflow-application-service.ts` +- `application/index.ts` + +**Step 1.5: 删除 infrastructure 层** + +删除 `packages/drawnix/src/infrastructure/` 目录(9 个文件): +- `infrastructure/execution/` (5 文件) +- `infrastructure/persistence/` (3 文件) +- `infrastructure/index.ts` (1 文件) + +**Step 1.6: 恢复原有服务** + +将 `packages/drawnix/src/types/task.types.ts` 改回直接定义或 re-export 共享类型。 + +#### 验证 + +```bash +# 类型检查 +cd packages/drawnix && npx tsc --noEmit + +# 确保 SW 构建成功 +pnpm run build + +# 运行测试 +pnpm nx test drawnix +``` + +#### 预期结果 + +- **删除**: 30 个文件(domain/application/infrastructure) +- **添加**: 1 个文件(shared/core.types.ts) +- **净变化**: -29 文件 +- **代码行数**: 减少 ~2500 行 + +--- + +--- + +### 方案 2: 评估 Workflow 服务结构(解决问题 2) + +#### 当前服务分析 + +经过代码审查,发现 3 个 Workflow 服务实际上有不同的职责: + +1. **workflow-submission-service.ts** (主服务) + - 提交工作流到 SW + - 监听 SW 事件(通过 swChannelClient) + - 处理主线程工具请求 + - 处理画布操作请求 + - 管理降级模式(MainThreadWorkflowEngine) + +2. **workflow-polling-service.ts** (辅助服务) + - 轮询 IndexedDB 中的工作流 + - 执行标记为 `pending_main_thread` 的步骤 + - 依赖 workflow-submission-service + +3. **workflow-completion-service.ts** (追踪服务) + - 追踪后处理状态(拆分、插入画布) + - 发布完成事件 + - 独立的 RxJS Subject + +#### 问题重新评估 + +**原计划**: 合并 3 个服务为 1 个 +**重新评估**: 这 3 个服务职责不同,不应强制合并 + +**真正的问题**: +- polling-service 依赖 submission-service,耦合度高 +- completion-service 独立追踪状态,但与其他服务缺少协调 +- 事件流分散(submission 的 RxJS Subject + completion 的 Subject) + +#### 优化方案 + +**不合并服务**,而是优化协调机制: + +1. **明确服务边界** + - submission-service: 工作流生命周期管理(提交、监听、恢复) + - polling-service: 主线程步骤执行器(可选,仅在需要时启用) + - completion-service: 后处理追踪器(可选,仅追踪需要后处理的工作流) + +2. **统一事件流**(见方案 3) + - 所有服务发布到统一的事件总线 + - 避免直接服务间调用 + +3. **解除循环依赖** + - polling-service 不应导入 submission-service + - 通过事件或共享存储通信 + +#### 实施步骤 + +**Step 2.1: 解除 polling-service 对 submission-service 的依赖** + +当前代码: +```typescript +// workflow-polling-service.ts +import { workflowSubmissionService, type WorkflowDefinition } from './workflow-submission-service'; +``` + +修改为通过事件通信: +```typescript +// workflow-polling-service.ts +import { workflowEvents$ } from './workflow-events'; // 统一事件流 + +// 监听工作流提交事件,而不是直接调用 submission-service +workflowEvents$.pipe( + filter(e => e.type === 'WORKFLOW_SUBMITTED') +).subscribe(event => { + // 开始轮询该工作流 +}); +``` + +**Step 2.2: 标准化服务接口** + +为每个服务定义清晰的公共接口: + +```typescript +// workflow-submission-service.ts +export interface WorkflowSubmissionService { + // 提交工作流 + submitWorkflow(definition: WorkflowDefinition): Promise; + + // 恢复工作流 + recoverWorkflows(boardId: string): Promise; + + // 取消工作流 + cancelWorkflow(workflowId: string): Promise; + + // 事件流(只读) + readonly events$: Observable; +} +``` + +#### 预期结果 + +- **不删除文件** - 3 个服务保持独立 +- **解除耦合** - 通过事件通信,不直接导入 +- **明确边界** - 每个服务职责清晰 +- **代码行数**: 减少 ~100 行(删除重复的类型定义和耦合代码) + +--- + +### 方案 3: 统一事件类型(解决问题 3) + +#### 目标 + +- 定义标准的事件类型 +- 使用单一事件流(而非多个 Subject) +- 类型安全的事件订阅 + +#### 实施步骤 + +**Step 3.1: 创建事件类型定义** + +创建 `packages/drawnix/src/types/shared/events.types.ts`: + +```typescript +/** + * 统一事件类型定义 + * + * 所有服务发布的事件都应符合这些类型。 + */ + +import type { Task, TaskStatus } from './core.types'; + +// ============================================================================ +// 事件基类 +// ============================================================================ + +export interface BaseEvent { + type: string; + timestamp: number; + source: string; // 事件来源(如 'task-queue', 'workflow-submission') +} + +// ============================================================================ +// Task 事件 +// ============================================================================ + +export interface TaskCreatedEvent extends BaseEvent { + type: 'TASK_CREATED'; + task: Task; +} + +export interface TaskStatusChangedEvent extends BaseEvent { + type: 'TASK_STATUS_CHANGED'; + taskId: string; + oldStatus: TaskStatus; + newStatus: TaskStatus; + task: Task; +} + +export interface TaskCompletedEvent extends BaseEvent { + type: 'TASK_COMPLETED'; + task: Task; +} + +export interface TaskFailedEvent extends BaseEvent { + type: 'TASK_FAILED'; + task: Task; + error: string; +} + +export type TaskEvent = + | TaskCreatedEvent + | TaskStatusChangedEvent + | TaskCompletedEvent + | TaskFailedEvent; + +// ============================================================================ +// Workflow 事件 +// ============================================================================ + +export interface WorkflowSubmittedEvent extends BaseEvent { + type: 'WORKFLOW_SUBMITTED'; + workflowId: string; + boardId?: string; +} + +export interface WorkflowStepCompletedEvent extends BaseEvent { + type: 'WORKFLOW_STEP_COMPLETED'; + workflowId: string; + stepId: string; +} + +export interface WorkflowCompletedEvent extends BaseEvent { + type: 'WORKFLOW_COMPLETED'; + workflowId: string; +} + +export interface WorkflowFailedEvent extends BaseEvent { + type: 'WORKFLOW_FAILED'; + workflowId: string; + error: string; +} + +export type WorkflowEvent = + | WorkflowSubmittedEvent + | WorkflowStepCompletedEvent + | WorkflowCompletedEvent + | WorkflowFailedEvent; + +// ============================================================================ +// 所有事件联合类型 +// ============================================================================ + +export type AppEvent = TaskEvent | WorkflowEvent; +``` + +**Step 3.2: 创建统一事件流** + +创建 `packages/drawnix/src/services/app-events.ts`: + +```typescript +/** + * 统一应用事件流 + * + * 所有服务通过这个事件流发布和订阅事件。 + */ + +import { Subject } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import type { AppEvent, TaskEvent, WorkflowEvent } from '../types/shared/events.types'; + +/** + * 全局事件流 + */ +const appEvents$ = new Subject(); + +/** + * 发布事件 + */ +export function publishEvent(event: Omit): void { + appEvents$.next({ + ...event, + timestamp: Date.now(), + } as AppEvent); +} + +/** + * 订阅所有事件 + */ +export function subscribeToEvents(handler: (event: AppEvent) => void) { + return appEvents$.subscribe(handler); +} + +/** + * 订阅特定类型的事件 + */ +export function subscribeToEventType( + type: T, + handler: (event: Extract) => void +) { + return appEvents$.pipe( + filter((e): e is Extract => e.type === type) + ).subscribe(handler); +} + +/** + * 订阅 Task 事件 + */ +export function subscribeToTaskEvents(handler: (event: TaskEvent) => void) { + return appEvents$.pipe( + filter((e): e is TaskEvent => + e.type.startsWith('TASK_') + ) + ).subscribe(handler); +} + +/** + * 订阅 Workflow 事件 + */ +export function subscribeToWorkflowEvents(handler: (event: WorkflowEvent) => void) { + return appEvents$.pipe( + filter((e): e is WorkflowEvent => + e.type.startsWith('WORKFLOW_') + ) + ).subscribe(handler); +} + +/** + * 导出只读的事件流(用于高级用法) + */ +export const events$ = appEvents$.asObservable(); +``` + +**Step 3.3: 更新服务使用统一事件流** + +示例 - 更新 task queue service: + +```typescript +// sw-task-queue-service.ts +import { publishEvent } from './app-events'; + +class SWTaskQueueService { + async createTask(type: TaskType, params: GenerationParams): Promise { + const task = { /* ... */ }; + + // 发布事件 + publishEvent({ + type: 'TASK_CREATED', + source: 'sw-task-queue', + task, + }); + + return task; + } +} +``` + +#### 预期结果 + +- **添加**: 2 个文件(events.types.ts, app-events.ts) +- **修改**: 各服务改用统一事件流 +- **删除**: 各服务内部的独立 Subject +- **代码行数**: 净增加 ~200 行(事件定义),但提高了类型安全性 + +--- + +## 实施路线图 + +### 阶段 1: 回滚 DDD 重构(1-2 小时) + +**优先级**: 最高 - 立即执行 + +1. 删除 `domain/` 目录(18 文件) +2. 删除 `application/` 目录(3 文件) +3. 删除 `infrastructure/` 目录(9 文件) +4. 恢复 `types/task.types.ts` 为直接定义 +5. 验证构建和测试通过 + +**验证**: +```bash +pnpm run build +pnpm nx test drawnix +``` + +### 阶段 2: 统一类型定义(2-3 小时) + +**优先级**: 高 + +1. 创建 `types/shared/core.types.ts` +2. 更新 SW 端导入 +3. 更新主线程导入 +4. 删除重复定义 +5. 验证类型检查通过 + +**验证**: +```bash +cd packages/drawnix && npx tsc --noEmit +``` + +### 阶段 3: 标准化事件(2-3 小时) + +**优先级**: 中 + +1. 创建 `types/shared/events.types.ts` +2. 创建 `services/app-events.ts` +3. 更新各服务使用统一事件流 +4. 验证事件流工作正常 + +**验证**: +- 手动测试任务创建和完成 +- 检查事件是否正确发布 + +### 阶段 4: 优化服务协调(可选,3-4 小时) + +**优先级**: 低 + +1. 解除 polling-service 对 submission-service 的依赖 +2. 标准化服务接口 +3. 添加服务文档 + +--- + +## 总结对比 + +| 指标 | DDD 方案 | 简化方案 | +|------|---------|---------| +| 新增文件 | +30 | +3 | +| 删除文件 | 0 | -30 | +| 净变化 | +30 | -27 | +| 代码行数 | +3000 | -2300 | +| 实施时间 | 2-3 周 | 1-2 天 | +| 风险 | 高(大规模重构) | 低(渐进式优化) | +| 复杂度 | 增加(多层抽象) | 降低(删除冗余) | + +## 下一步行动 + +1. **立即执行**: 回滚 DDD 重构(阶段 1) +2. **本周完成**: 统一类型定义(阶段 2) +3. **下周完成**: 标准化事件(阶段 3) +4. **可选**: 优化服务协调(阶段 4) + +每个阶段独立可测试,可随时停止或调整。 diff --git a/.codebuddy/plans/task-workflow-ddd-architecture_8cdf8a89.md b/.codebuddy/plans/task-workflow-ddd-architecture_8cdf8a89.md new file mode 100644 index 00000000..2b14c891 --- /dev/null +++ b/.codebuddy/plans/task-workflow-ddd-architecture_8cdf8a89.md @@ -0,0 +1,585 @@ +--- +name: task-workflow-ddd-architecture +overview: 引入领域驱动开发(DDD)架构来重构任务和工作流系统,通过定义清晰的领域模型、聚合根、领域事件和应用服务来降低复杂度,收敛领域知识,便于后续扩展。 +todos: + - id: create-domain-task + content: 创建 Task 领域模型:实现 Task 聚合根、TaskStatus/TaskType 值对象、TaskRepository 接口,统一 3 处重复的类型定义 + status: completed + - id: create-domain-workflow + content: 创建 Workflow 领域模型:实现 Workflow 聚合根、WorkflowStep 实体、WorkflowRepository 接口 + status: completed + - id: create-domain-events + content: 创建领域事件系统:实现 DomainEvent 基类、Task/Workflow 相关事件类、DomainEventBus 单例 + status: completed + dependencies: + - create-domain-task + - create-domain-workflow + - id: create-execution-strategies + content: 创建执行策略模块:实现 TaskExecutionStrategy 接口、SWExecutionStrategy、MainThreadExecutionStrategy、ExecutionStrategySelector + status: completed + dependencies: + - create-domain-task + - id: create-repositories + content: 创建仓储实现:实现 IndexedDBTaskRepository、IndexedDBWorkflowRepository,封装现有 storage reader/writer + status: completed + dependencies: + - create-domain-task + - create-domain-workflow + - id: create-application-services + content: 创建应用服务层:实现 TaskApplicationService、WorkflowApplicationService,整合领域逻辑和基础设施 + status: completed + dependencies: + - create-domain-events + - create-execution-strategies + - create-repositories + - id: create-react-integration + content: 创建 React 集成层:实现 useDomainEvent Hook,将领域事件桥接到 React 组件 + status: completed + dependencies: + - create-domain-events +--- + +## 用户需求 + +对当前项目的任务(Task)和工作流(Workflow)相关逻辑进行架构分析,引入领域驱动设计(DDD)方式来降低复杂度,收敛领域知识,并便于后续扩展。 + +## 产品概述 + +当前系统是一个 AI 图像/视频生成工具,核心功能涉及: + +- **任务队列管理**:支持图片、视频、角色提取、灵感板等多种生成任务 +- **工作流编排**:支持多步骤 MCP 工具调用的工作流执行 +- **双模式执行**:Service Worker 模式(主用)和主线程降级模式 +- **状态同步**:主线程与 SW 之间的双向通信和状态同步 + +## 核心问题分析 + +### 1. 类型定义散落重复(3处) + +- `packages/drawnix/src/types/task.types.ts` - 主线程类型 +- `apps/web/src/sw/task-queue/types.ts` - SW 端类型 +- `packages/drawnix/src/services/sw-channel/types/` - 通信层类型 +- 依赖注释 "Keep in sync with..." 维护一致性,极易出错 + +### 2. 双模式维护成本高 + +- SW 模式:`swTaskQueueService` + `SWTaskQueue` + `WorkflowExecutor` +- 降级模式:`taskQueueService` + `WorkflowEngine` +- 两套几乎相同的任务执行和状态管理逻辑 + +### 3. 工作流状态分散 + +- `WorkflowContext` - React 状态 +- `WorkflowSubmissionService` - 提交服务 +- `WorkflowPollingService` - 轮询服务 +- `WorkflowCompletionService` - 后处理追踪 +- `workflowStorageReader/Writer` - 存储服务 +- 各服务独立维护状态,难以追踪完整生命周期 + +### 4. 职责边界模糊 + +- `swTaskQueueService` 既管理任务状态,又负责 SW 通信 +- `workflowSubmissionService` 既负责提交,又处理恢复和事件分发 +- 缺乏清晰的领域边界 + +### 5. 缺乏统一领域事件 + +- 各服务使用独立 RxJS Subject,事件类型不统一 +- 跨服务协调依赖直接方法调用,耦合度高 + +## 设计目标 + +1. **统一领域模型**:建立任务和工作流的单一真相来源(Single Source of Truth) +2. **清晰边界划分**:应用 DDD 分层架构,分离领域逻辑与基础设施 +3. **降低耦合度**:通过领域事件实现服务间松耦合通信 +4. **消除重复**:统一类型定义,抽象共享行为 +5. **可扩展性**:支持新任务类型和工作流步骤的低成本接入 + +## 技术栈 + +- **语言**:TypeScript(严格模式) +- **状态管理**:RxJS(领域事件流) +- **持久化**:IndexedDB(通过 localforage) +- **通信**:postmessage-duplex(SW 双工通信) +- **UI 框架**:React + Context API + +## 实现方案 + +### DDD 分层架构设计 + +采用经典 DDD 四层架构,适配当前 Service Worker + 主线程的双模式场景: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ (React Hooks/Context, WorkflowSubmissionService) │ +├─────────────────────────────────────────────────────────────┤ +│ Domain Layer (NEW) │ +│ Task Aggregate │ Workflow Aggregate │ Domain Events │ +├─────────────────────────────────────────────────────────────┤ +│ Infrastructure Layer │ +│ SW Channel │ Storage │ API Services │ Executors │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1. 统一领域模型(Domain Layer) + +#### 核心聚合根 + +``` +packages/drawnix/src/domain/ +├── task/ +│ ├── task.ts # Task 聚合根(值对象 + 行为) +│ ├── task-status.ts # TaskStatus 值对象 +│ ├── task-type.ts # TaskType 值对象 +│ └── task-repository.ts # 仓储接口 +├── workflow/ +│ ├── workflow.ts # Workflow 聚合根 +│ ├── workflow-step.ts # WorkflowStep 实体 +│ └── workflow-repository.ts # 仓储接口 +├── events/ +│ ├── domain-event.ts # 领域事件基类 +│ ├── task-events.ts # 任务相关事件 +│ ├── workflow-events.ts # 工作流相关事件 +│ └── event-bus.ts # 统一事件总线 +└── index.ts # 公共导出 +``` + +#### 设计要点 + +**Task 聚合根**:封装任务状态机和业务规则 + +- 状态转换:`PENDING → PROCESSING → COMPLETED/FAILED/CANCELLED` +- 执行阶段:`SUBMITTING → POLLING → DOWNLOADING` +- 业务规则:只有 FAILED/CANCELLED 状态的任务可重试 + +**Workflow 聚合根**:封装工作流编排逻辑 + +- 步骤依赖解析和并行执行 +- 动态步骤添加(AI 分析后添加) +- 主线程/SW 步骤分类和路由 + +**领域事件**:统一的事件契约 + +- `TaskCreated`, `TaskStatusChanged`, `TaskCompleted`, `TaskFailed` +- `WorkflowStarted`, `WorkflowStepCompleted`, `WorkflowCompleted` + +### 2. 仓储模式(Repository Pattern) + +抽象存储访问,隔离 IndexedDB 细节: + +```typescript +// 仓储接口(领域层) +interface TaskRepository { + findById(id: string): Promise; + findByStatus(status: TaskStatus): Promise; + save(task: Task): Promise; + delete(id: string): Promise; +} + +// 实现(基础设施层) +class IndexedDBTaskRepository implements TaskRepository { + // 封装 taskStorageReader/Writer +} +``` + +### 3. 执行策略模式(Strategy Pattern) + +统一 SW 和主线程执行器接口: + +```typescript +// 执行策略接口 +interface TaskExecutionStrategy { + execute(task: Task): Promise; + cancel(taskId: string): Promise; + resume(task: Task): Promise; +} + +// SW 执行策略 +class SWExecutionStrategy implements TaskExecutionStrategy { } + +// 主线程降级执行策略 +class MainThreadExecutionStrategy implements TaskExecutionStrategy { } + +// 策略选择器 +class ExecutionStrategySelector { + async getStrategy(): Promise { + return await this.isSWAvailable() + ? this.swStrategy + : this.mainThreadStrategy; + } +} +``` + +### 4. 统一事件总线 + +替代分散的 RxJS Subject: + +```typescript +// 领域事件基类 +abstract class DomainEvent { + readonly eventId: string; + readonly occurredAt: number; + readonly aggregateId: string; +} + +// 事件总线 +class DomainEventBus { + private subject = new Subject(); + + publish(event: DomainEvent): void { + this.subject.next(event); + } + + subscribe( + eventType: new (...args: any[]) => T, + handler: (event: T) => void + ): Subscription { + return this.subject.pipe( + filter((e): e is T => e instanceof eventType) + ).subscribe(handler); + } + + // 支持按聚合 ID 过滤 + subscribeToAggregate( + aggregateId: string, + handler: (event: DomainEvent) => void + ): Subscription { } +} +``` + +### 5. 应用服务重构 + +简化现有服务,专注协调领域操作: + +```typescript +// 重构后的 TaskApplicationService +class TaskApplicationService { + constructor( + private taskRepo: TaskRepository, + private executionStrategy: ExecutionStrategySelector, + private eventBus: DomainEventBus + ) {} + + async createTask(params: CreateTaskParams): Promise { + const task = Task.create(params); + await this.taskRepo.save(task); + this.eventBus.publish(new TaskCreated(task)); + + // 异步执行 + this.executeTask(task); + return task; + } + + private async executeTask(task: Task): Promise { + const strategy = await this.executionStrategy.getStrategy(); + try { + const result = await strategy.execute(task); + task.complete(result); + this.eventBus.publish(new TaskCompleted(task)); + } catch (error) { + task.fail(error); + this.eventBus.publish(new TaskFailed(task, error)); + } + await this.taskRepo.save(task); + } +} +``` + +## 实现说明 + +### 迁移策略 + +采用**渐进式重构**,不一次性重写: + +1. **Phase 1**:建立领域层,新代码使用新模型 +2. **Phase 2**:逐步迁移现有服务到新架构 +3. **Phase 3**:废弃旧代码,统一到新模型 + +### 关键设计决策 + +1. **类型统一**:领域层定义唯一类型,SW/主线程通过序列化/反序列化共享 +2. **事件溯源预留**:领域事件设计支持未来引入事件溯源 +3. **SW 边界清晰**:SW 只作为执行器,领域逻辑在主线程 +4. **React 集成**:通过 `useDomainEvent` Hook 将领域事件桥接到 React + +### 性能考量 + +- 领域事件使用 RxJS Subject,内存占用可控 +- 仓储层实现缓存策略,减少 IndexedDB 访问 +- 大批量任务支持分页加载 + +## 架构设计 + +### 系统架构图 + +```mermaid +graph TB + subgraph "Application Layer" + RC[React Components] + HC[Hooks & Contexts] + AS[Application Services] + end + + subgraph "Domain Layer" + TA[Task Aggregate] + WA[Workflow Aggregate] + EB[Event Bus] + DE[Domain Events] + end + + subgraph "Infrastructure Layer" + TR[Task Repository] + WR[Workflow Repository] + SS[SW Execution Strategy] + MS[MainThread Strategy] + SC[SW Channel] + DB[(IndexedDB)] + end + + RC --> HC + HC --> AS + AS --> TA + AS --> WA + AS --> EB + TA --> DE + WA --> DE + DE --> EB + + TA --> TR + WA --> WR + TR --> DB + WR --> DB + + AS --> SS + AS --> MS + SS --> SC + SC -->|postMessage| SW[Service Worker] +``` + +### 领域模型类图 + +```mermaid +classDiagram + class Task { + +id: string + +type: TaskType + +status: TaskStatus + +params: GenerationParams + +result?: TaskResult + +error?: TaskError + +create(params): Task + +start(): void + +complete(result): void + +fail(error): void + +cancel(): void + +canRetry(): boolean + } + + class Workflow { + +id: string + +steps: WorkflowStep[] + +status: WorkflowStatus + +context: WorkflowContext + +addStep(step): void + +updateStepStatus(stepId, status): void + +getExecutableSteps(): WorkflowStep[] + +isCompleted(): boolean + } + + class WorkflowStep { + +id: string + +mcp: string + +args: Record + +status: StepStatus + +dependsOn?: string[] + +taskId?: string + } + + class DomainEvent { + <> + +eventId: string + +occurredAt: number + +aggregateId: string + } + + class TaskCreated { + +task: Task + } + + class TaskCompleted { + +task: Task + +result: TaskResult + } + + Task --> TaskStatus + Task --> TaskType + Workflow --> WorkflowStep + Workflow --> WorkflowStatus + WorkflowStep --> Task : creates + DomainEvent <|-- TaskCreated + DomainEvent <|-- TaskCompleted +``` + +## 目录结构 + +``` +packages/drawnix/src/ +├── domain/ # [NEW] 领域层 - 核心业务逻辑 +│ ├── task/ +│ │ ├── task.ts # Task 聚合根。封装任务状态机(PENDING→PROCESSING→COMPLETED/FAILED),包含 create()、start()、complete()、fail()、cancel() 等状态转换方法,以及 canRetry() 等业务规则校验。 +│ │ ├── task-status.ts # TaskStatus 值对象。定义任务状态枚举,统一主线程和 SW 的状态定义,消除当前 3 处重复定义的问题。 +│ │ ├── task-type.ts # TaskType 值对象。定义任务类型(IMAGE/VIDEO/CHARACTER/INSPIRATION_BOARD/CHAT),包含类型相关的配置(超时时间等)。 +│ │ ├── task-result.ts # TaskResult 值对象。封装任务结果数据结构,支持不同类型任务的结果格式。 +│ │ ├── task-repository.ts # TaskRepository 接口。定义任务仓储契约(findById/findByStatus/save/delete),隔离持久化细节。 +│ │ └── index.ts # Task 模块导出。 +│ ├── workflow/ +│ │ ├── workflow.ts # Workflow 聚合根。封装工作流状态管理和步骤编排逻辑,包含 addStep()、updateStepStatus()、getExecutableSteps() 等方法。 +│ │ ├── workflow-step.ts # WorkflowStep 实体。定义工作流步骤结构,包含依赖关系、执行状态、关联的任务 ID 等。 +│ │ ├── workflow-status.ts # WorkflowStatus 值对象。定义工作流状态枚举(pending/running/completed/failed/cancelled)。 +│ │ ├── workflow-repository.ts # WorkflowRepository 接口。定义工作流仓储契约。 +│ │ └── index.ts # Workflow 模块导出。 +│ ├── events/ +│ │ ├── domain-event.ts # DomainEvent 基类。定义所有领域事件的公共属性(eventId、occurredAt、aggregateId),提供事件序列化能力。 +│ │ ├── task-events.ts # 任务领域事件。包含 TaskCreated、TaskStatusChanged、TaskCompleted、TaskFailed、TaskCancelled 等事件类。 +│ │ ├── workflow-events.ts # 工作流领域事件。包含 WorkflowStarted、WorkflowStepStatusChanged、WorkflowStepCompleted、WorkflowCompleted、WorkflowFailed 等事件类。 +│ │ ├── event-bus.ts # DomainEventBus 单例。统一的事件发布/订阅中心,替代分散的 RxJS Subject,支持按事件类型和聚合 ID 过滤订阅。 +│ │ └── index.ts # Events 模块导出。 +│ ├── shared/ +│ │ ├── generation-params.ts # GenerationParams 值对象。统一的生成参数定义,被 Task 和 Workflow 共享。 +│ │ └── index.ts # Shared 模块导出。 +│ └── index.ts # 领域层统一导出。 +├── application/ # [NEW] 应用服务层 - 用例编排 +│ ├── task-application-service.ts # TaskApplicationService。协调任务创建、执行、状态更新的用例,使用仓储和执行策略,发布领域事件。替代当前 swTaskQueueService 中的业务逻辑。 +│ ├── workflow-application-service.ts # WorkflowApplicationService。协调工作流提交、步骤执行、状态同步的用例,整合当前 workflowSubmissionService、workflowPollingService、workflowCompletionService 的职责。 +│ └── index.ts # 应用服务层导出。 +├── infrastructure/ # [MODIFY] 基础设施层 - 重组现有服务 +│ ├── persistence/ +│ │ ├── indexeddb-task-repository.ts # IndexedDBTaskRepository。实现 TaskRepository 接口,封装 taskStorageReader/Writer,提供任务持久化能力。 +│ │ ├── indexeddb-workflow-repository.ts # IndexedDBWorkflowRepository。实现 WorkflowRepository 接口,封装 workflowStorageReader/Writer。 +│ │ └── index.ts # 持久化模块导出。 +│ ├── execution/ +│ │ ├── execution-strategy.ts # TaskExecutionStrategy 接口。定义任务执行策略契约(execute/cancel/resume)。 +│ │ ├── sw-execution-strategy.ts # SWExecutionStrategy。通过 SW Channel 执行任务的策略实现,封装 swChannelClient 调用。 +│ │ ├── mainthread-execution-strategy.ts # MainThreadExecutionStrategy。主线程降级执行策略,封装 media-executor 调用。 +│ │ ├── execution-strategy-selector.ts # ExecutionStrategySelector。根据 SW 可用性自动选择执行策略的选择器。 +│ │ └── index.ts # 执行策略模块导出。 +│ └── index.ts # 基础设施层导出。 +├── hooks/ +│ ├── useDomainEvent.ts # [NEW] useDomainEvent Hook。将领域事件桥接到 React,订阅指定类型的事件并触发组件更新。 +│ ├── useTask.ts # [MODIFY] 重构为使用 TaskApplicationService 和 useDomainEvent,简化状态管理逻辑。 +│ └── useWorkflow.ts # [MODIFY] 重构为使用 WorkflowApplicationService 和 useDomainEvent。 +└── services/ + ├── sw-task-queue-service.ts # [MODIFY] 简化为薄层,委托给 TaskApplicationService,保留向后兼容的 API。 + ├── workflow-submission-service.ts # [MODIFY] 简化为薄层,委托给 WorkflowApplicationService。 + └── ... # 其他服务保持不变或逐步迁移。 +``` + +## 核心代码结构 + +### Task 聚合根接口 + +```typescript +/** + * Task 聚合根 + * 封装任务状态机和业务规则 + */ +export class Task { + private constructor( + public readonly id: string, + public readonly type: TaskType, + private _status: TaskStatus, + public readonly params: GenerationParams, + public readonly createdAt: number, + private _updatedAt: number, + private _startedAt?: number, + private _completedAt?: number, + private _result?: TaskResult, + private _error?: TaskError, + private _progress?: number, + private _executionPhase?: TaskExecutionPhase + ) {} + + // 工厂方法 + static create(params: CreateTaskParams): Task; + + // 状态查询 + get status(): TaskStatus; + get result(): TaskResult | undefined; + get error(): TaskError | undefined; + + // 状态转换 + start(): void; + updateProgress(progress: number, phase?: TaskExecutionPhase): void; + complete(result: TaskResult): void; + fail(error: TaskError): void; + cancel(): void; + + // 业务规则 + canRetry(): boolean; + isTerminal(): boolean; +} +``` + +### DomainEventBus 接口 + +```typescript +/** + * 领域事件总线 + * 统一的事件发布/订阅中心 + */ +export class DomainEventBus { + private static instance: DomainEventBus; + private subject = new Subject(); + + static getInstance(): DomainEventBus; + + // 发布事件 + publish(event: DomainEvent): void; + + // 按类型订阅 + subscribe( + eventType: new (...args: any[]) => T, + handler: (event: T) => void + ): Subscription; + + // 按聚合 ID 订阅 + subscribeToAggregate( + aggregateId: string, + handler: (event: DomainEvent) => void + ): Subscription; + + // 订阅所有事件(调试用) + subscribeAll(handler: (event: DomainEvent) => void): Subscription; +} +``` + +### ExecutionStrategySelector 接口 + +```typescript +/** + * 执行策略选择器 + * 自动选择 SW 或主线程执行策略 + */ +export class ExecutionStrategySelector { + constructor( + private swStrategy: SWExecutionStrategy, + private mainThreadStrategy: MainThreadExecutionStrategy + ) {} + + async getStrategy(): Promise { + const swAvailable = await this.checkSWAvailability(); + return swAvailable ? this.swStrategy : this.mainThreadStrategy; + } + + private async checkSWAvailability(): Promise; +} +``` + +## Agent Extensions + +### SubAgent + +- **code-explorer** +- 目的:在实现阶段深度探索现有服务的具体实现细节,包括 SW 端的 handlers、存储实现、通信协议等 +- 预期结果:获取完整的代码上下文,确保新领域层与现有基础设施的正确集成 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index bb299c93..8b860e04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,6 +147,9 @@ Service Worker (后台执行) 16. **数据安全保护**:破坏性操作(删除、覆盖)前必须安全检查,当前编辑项不可删除,批量删除需确认,不可逆操作需输入确认文字 17. **模块循环依赖**:将共享函数(如 `shouldUseSWTaskQueue`)提取到独立模块打破循环;单例构造函数用 `queueMicrotask` 延迟访问其他模块导出 18. **配置对象深拷贝**:`getSetting()` 等返回配置对象的方法需返回深拷贝,防止脱敏函数等外部操作修改全局状态 +19. **避免过度设计**:优先使用简单的 interface + service 模式;只在有明确需求时才添加 Repository、Strategy 等抽象层 +20. **删除未使用代码**:未使用的"基础设施"代码应删除,不要"为未来预留";遵循 YAGNI 原则 +21. **重构先问问题**:重构前明确要解决的实际问题;验证方案是否真的解决问题而非增加复杂度 ### Service Worker 规则 diff --git a/apps/web/src/sw/task-queue/types.ts b/apps/web/src/sw/task-queue/types.ts index eb820494..9ea445c4 100644 --- a/apps/web/src/sw/task-queue/types.ts +++ b/apps/web/src/sw/task-queue/types.ts @@ -4,112 +4,27 @@ * Defines all types for SW-based task queue management and * postMessage communication between main thread and Service Worker. * - * Note: Types are defined independently here to avoid import issues in SW context. - * Keep in sync with packages/drawnix/src/types/task.types.ts + * Core types are imported from the shared types module to ensure consistency. + * 核心类型从共享类型模块导入,确保与主线程一致。 */ -import type { ChatToolCall } from './chat-workflow/types'; - // ============================================================================ -// Task Enums and Core Types (mirrored from task.types.ts) +// Re-export Core Types from Shared Module // ============================================================================ -/** - * Task status enumeration - */ -export enum TaskStatus { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} +// Re-export all core types from shared module +export * from '../../../../../packages/drawnix/src/types/shared/core.types'; -/** - * Task type enumeration - */ -export enum TaskType { - IMAGE = 'image', - VIDEO = 'video', - CHARACTER = 'character', - INSPIRATION_BOARD = 'inspiration_board', - CHAT = 'chat', -} - -/** - * Task execution phase enumeration - */ -export enum TaskExecutionPhase { - SUBMITTING = 'submitting', - POLLING = 'polling', - DOWNLOADING = 'downloading', -} - -/** - * Generation parameters interface - */ -export interface GenerationParams { - prompt: string; - width?: number; - height?: number; - size?: string; - duration?: number; - style?: string; - model?: string; - seed?: number; - sourceVideoTaskId?: string; - characterTimestamps?: string; - sourceLocalTaskId?: string; - gridImageRows?: number; - gridImageCols?: number; - gridImageLayoutStyle?: 'scattered' | 'grid' | 'circular'; - inspirationBoardLayoutStyle?: 'inspiration-board'; - isInspirationBoard?: boolean; - inspirationBoardImageCount?: number; - /** Whether to auto-insert the result to canvas when task completes */ - autoInsertToCanvas?: boolean; - /** image aspect ratio token (e.g. 16:9) */ - aspectRatio?: string; - [key: string]: unknown; -} - -/** - * Task result interface - */ -export interface TaskResult { - url: string; - format: string; - size: number; - width?: number; - height?: number; - duration?: number; - thumbnailUrl?: string; - characterUsername?: string; - characterProfileUrl?: string; - characterPermalink?: string; - /** Chat response content (chat only) */ - chatResponse?: string; - /** Tool calls made during chat (chat only) */ - toolCalls?: ChatToolCall[]; -} - -/** - * Task error details interface - */ -export interface TaskErrorDetails { - originalError?: string; - apiResponse?: unknown; - timestamp?: number; -} - -/** - * Task error interface - */ -export interface TaskError { - code: string; - message: string; - details?: TaskErrorDetails; -} +// Import types for use in this file +import type { + ChatToolCall, + GenerationParams, + TaskResult, + TaskError, + TaskStatus, + TaskType, + TaskExecutionPhase, +} from '../../../../../packages/drawnix/src/types/shared/core.types'; // ============================================================================ // Task Config diff --git a/docs/CODING_RULES.md b/docs/CODING_RULES.md index a642ceeb..fca58bf8 100644 --- a/docs/CODING_RULES.md +++ b/docs/CODING_RULES.md @@ -8,6 +8,7 @@ ## 目录 +- [架构与重构规范](#架构与重构规范) - [文件命名规范](#文件命名规范) - [TypeScript 规范](#typescript-规范) - [React 组件规范](#react-组件规范) @@ -21,6 +22,237 @@ --- +## 架构与重构规范 + +### 避免过度设计和不必要的抽象层 + +**场景**: 在重构或添加新功能时,避免引入不必要的架构模式 + +**核心原则**: +- 优先使用简单的 interface + service 模式 +- 只在有明确需求时才添加抽象层 +- 遵循 YAGNI 原则(You Aren't Gonna Need It) + +❌ **错误示例 - 过度使用 DDD 模式**: +```typescript +// 错误:为简单的任务管理引入过多抽象层 + +// domain/task/task.ts (487 行) +export class Task { + private constructor(...) {} // 私有构造函数 + static create(...) {} // 工厂方法 + start() {} // 状态转换方法 + complete() {} + fail() {} + // ... 50+ 个方法 +} + +// domain/task/task-repository.ts +interface TaskRepository { + findById(id: string): Promise; + save(task: Task): Promise; +} + +// infrastructure/persistence/indexeddb-task-repository.ts +class IndexedDBTaskRepository implements TaskRepository { + // 只是包装了现有的 taskStorageReader/Writer +} + +// infrastructure/execution/execution-strategy.ts +interface TaskExecutionStrategy { + execute(task: Task): Promise; +} + +// infrastructure/execution/sw-execution-strategy.ts +class SWExecutionStrategy implements TaskExecutionStrategy { + // 包装了一个简单的条件判断 +} + +// 结果:30 个新文件,3000+ 行代码,但没有解决实际问题 +``` + +✅ **正确示例 - 保持简单**: +```typescript +// types/task.types.ts - 简单的接口定义 +export interface Task { + id: string; + type: TaskType; + status: TaskStatus; + params: GenerationParams; + // ... 其他字段 +} + +// services/task-queue-service.ts - 直接的服务实现 +export class TaskQueueService { + async createTask(params: GenerationParams): Promise { + const task = { /* ... */ }; + await taskStorageWriter.saveTask(task); + return task; + } + + async executeTask(task: Task): Promise { + // 简单的条件判断,不需要 Strategy 模式 + if (shouldUseSWTaskQueue()) { + await swTaskQueueService.createTask(task.type, task.params); + } else { + await taskQueueService.createTask(task.type, task.params); + } + } +} + +// 结果:清晰、简单、易维护 +``` + +**判断是否需要抽象层的标准**: + +1. **Repository 模式**: + - ❌ 不需要:只有一种存储实现(如只用 IndexedDB) + - ✅ 需要:需要支持多种存储后端(IndexedDB、LocalStorage、远程 API) + +2. **Strategy 模式**: + - ❌ 不需要:只是简单的 if/else 判断(如 `if (shouldUseSW)`) + - ✅ 需要:有 3+ 种算法需要动态切换,且逻辑复杂 + +3. **Factory 模式**: + - ❌ 不需要:对象创建逻辑简单(如 `{ id, type, status }`) + - ✅ 需要:创建逻辑复杂,需要根据类型创建不同的对象 + +4. **Aggregate Root**: + - ❌ 不需要:简单的数据结构,状态转换逻辑简单 + - ✅ 需要:复杂的业务规则,需要保证不变性约束 + +**原因**: +- 过度设计会增加代码量和维护成本 +- 抽象层会增加理解难度和调试复杂度 +- 简单的问题应该用简单的方案解决 +- 只在有明确需求时才引入复杂模式 + +--- + +### 删除未使用的"基础设施"代码 + +**场景**: 创建了"为未来准备"的代码,但实际上没有被使用 + +**核心原则**: +- 代码应该解决当前的实际问题 +- 未使用的代码应该删除,不要"为未来预留" +- 如果未来真的需要,可以从 Git 历史恢复 + +❌ **错误示例 - 创建未使用的事件系统**: +```typescript +// services/app-events.ts - 创建了统一事件总线 +export function publishEvent(event: AppEvent): void { } +export function subscribeToEventType(type: T, handler: Function) { } + +// types/events.types.ts - 定义了完整的事件类型 +export interface TaskCreatedEvent extends BaseEvent { } +export interface TaskCompletedEvent extends BaseEvent { } +// ... 10+ 个事件类型 + +// 问题:没有任何代码使用这些文件 +// grep -r "from.*app-events" 返回 0 个结果 +``` + +✅ **正确做法 - 删除未使用的代码**: +```bash +# 检查是否有代码使用 +grep -r "from.*app-events" packages/drawnix/src + +# 如果没有使用,直接删除 +rm src/services/app-events.ts +rm src/types/shared/events.types.ts + +# 如果未来需要,可以从 Git 历史恢复 +git log --all --full-history -- "**/app-events.ts" +``` + +**判断是否应该保留代码的标准**: + +1. **有实际使用**: + - ✅ 保留:至少有 1 处代码导入并使用 + - ❌ 删除:没有任何地方使用 + +2. **解决实际问题**: + - ✅ 保留:解决了当前存在的问题 + - ❌ 删除:只是"可能有用"的基础设施 + +3. **维护成本**: + - ✅ 保留:维护成本低,不会成为负担 + - ❌ 删除:需要持续维护,但没有实际价值 + +**原因**: +- 未使用的代码会增加维护负担 +- 会让新开发者困惑("这个是干什么的?") +- Git 历史可以保存所有删除的代码 +- YAGNI 原则:不要为未来可能的需求编写代码 + +--- + +### 重构前先问"解决什么问题" + +**场景**: 在进行架构重构时,应该先明确要解决的实际问题 + +**核心原则**: +- 重构应该解决具体的痛点,而不是追求"更好的架构" +- 先列出当前的实际问题,再设计解决方案 +- 验证重构是否真的解决了问题 + +❌ **错误示例 - 为了架构而重构**: +```typescript +// 问题描述:"代码不够优雅,需要引入 DDD" +// 实际情况:代码工作正常,没有明确的痛点 + +// 重构方案: +// - 添加 domain 层(18 个文件) +// - 添加 application 层(3 个文件) +// - 添加 infrastructure 层(9 个文件) + +// 结果: +// - 新增 30 个文件,3000+ 行代码 +// - 原有问题没有解决 +// - 引入了新的复杂度 +``` + +✅ **正确示例 - 针对问题重构**: +```typescript +// 问题 1:类型定义在 3 处重复,需要手动同步 +// 解决方案:创建共享类型文件 +// 结果:删除 2 处重复定义,统一到 1 个文件 + +// 问题 2:DDD 层增加了 30 个文件但没有解决实际问题 +// 解决方案:删除 DDD 层,回归简单的 interface + service +// 结果:删除 30 个文件,减少 3000 行代码 + +// 验证: +// - 类型定义只有 1 处 ✓ +// - 代码量减少 ✓ +// - 构建和测试通过 ✓ +``` + +**重构前的检查清单**: + +1. **明确问题**: + - [ ] 列出当前存在的具体问题 + - [ ] 问题是否真的影响开发效率或代码质量 + - [ ] 问题是否频繁出现 + +2. **评估方案**: + - [ ] 方案是否直接解决问题 + - [ ] 方案是否引入新的复杂度 + - [ ] 方案的成本是否合理 + +3. **验证结果**: + - [ ] 原有问题是否解决 + - [ ] 是否引入新问题 + - [ ] 代码是否更简单(文件数、行数) + +**原因**: +- 架构模式不是目的,解决问题才是 +- 好的架构应该让代码更简单,而不是更复杂 +- 重构应该有明确的收益,而不是"看起来更好" + +--- + ### 文件命名规范 - **组件**: `PascalCase.tsx` (如 `ImageCropPopup.tsx`) diff --git a/packages/drawnix/src/types/shared/core.types.ts b/packages/drawnix/src/types/shared/core.types.ts new file mode 100644 index 00000000..a4e8d2bd --- /dev/null +++ b/packages/drawnix/src/types/shared/core.types.ts @@ -0,0 +1,290 @@ +/** + * Core Type Definitions - Shared between Main Thread and Service Worker + * + * This is the single source of truth for task and workflow types. + * Both main thread and SW import from here to ensure type consistency. + * + * 这是任务和工作流类型的唯一真相来源。 + * 主线程和 SW 都从这里导入,确保类型一致性。 + */ + +// ============================================================================ +// Chat Tool Call +// ============================================================================ + +/** + * Chat tool call interface + * Represents a tool call made during AI chat/analysis + */ +export interface ChatToolCall { + /** Tool name (e.g., 'generate_image', 'generate_video') */ + name: string; + /** Tool arguments */ + arguments: Record; + /** Tool call result */ + result?: { + success: boolean; + taskId?: string; + error?: string; + data?: unknown; + }; +} + +// ============================================================================ +// Task Enums +// ============================================================================ + +/** + * Task status enumeration + * Represents all possible states a task can be in during its lifecycle + */ +export enum TaskStatus { + /** Task is waiting to be executed */ + PENDING = 'pending', + /** Task is currently being processed */ + PROCESSING = 'processing', + /** Task completed successfully */ + COMPLETED = 'completed', + /** Task failed */ + FAILED = 'failed', + /** Task was cancelled by the user */ + CANCELLED = 'cancelled' +} + +/** + * Task type enumeration + * Defines the types of content that can be generated + */ +export enum TaskType { + /** Image generation task */ + IMAGE = 'image', + /** Video generation task */ + VIDEO = 'video', + /** Character extraction task */ + CHARACTER = 'character', + /** Inspiration board generation task (image + split + layout) */ + INSPIRATION_BOARD = 'inspiration_board', + /** Chat/AI analysis task (text model streaming) */ + CHAT = 'chat', +} + +/** + * Task execution phase enumeration + * Used for tracking async task progress and enabling recovery after page refresh + */ +export enum TaskExecutionPhase { + /** Task is being submitted to the API */ + SUBMITTING = 'submitting', + /** Task submitted, polling for completion (video only) */ + POLLING = 'polling', + /** Task completed, downloading result */ + DOWNLOADING = 'downloading' +} + +// ============================================================================ +// Generation Parameters +// ============================================================================ + +/** + * Generation parameters interface + * Contains all parameters needed for AI content generation + */ +export interface GenerationParams { + /** Text prompt describing the desired content */ + prompt: string; + /** Image/video width in pixels */ + width?: number; + /** Image/video height in pixels */ + height?: number; + /** Size parameter for API (e.g., '16x9', '1x1') */ + size?: string; + /** Video duration in seconds (video only) */ + duration?: number; + /** Style or model to use for generation */ + style?: string; + /** AI model to use (e.g., 'veo3', 'sora-2') */ + model?: string; + /** Random seed for reproducible generation */ + seed?: number; + /** Source video task ID for character extraction */ + sourceVideoTaskId?: string; + /** Time range for character extraction (format: "start,end") */ + characterTimestamps?: string; + /** Local task ID of source video */ + sourceLocalTaskId?: string; + /** Grid image grid rows (grid_image only) */ + gridImageRows?: number; + /** Grid image grid columns (grid_image only) */ + gridImageCols?: number; + /** Grid image layout style (grid_image only) */ + gridImageLayoutStyle?: 'scattered' | 'grid' | 'circular'; + /** Inspiration board layout style (inspiration_board only) */ + inspirationBoardLayoutStyle?: 'inspiration-board'; + /** Whether this is an inspiration board task */ + isInspirationBoard?: boolean; + /** Inspiration board image count */ + inspirationBoardImageCount?: number; + /** Whether to auto-insert the result to canvas when task completes */ + autoInsertToCanvas?: boolean; + /** Additional parameters for specific generation types */ + [key: string]: any; +} + +// ============================================================================ +// Task Result +// ============================================================================ + +/** + * Task result interface + * Contains the output from a successfully completed task + */ +export interface TaskResult { + /** URL to the generated content */ + url: string; + /** File format (e.g., 'png', 'jpg', 'mp4') */ + format: string; + /** File size in bytes */ + size: number; + /** Content width in pixels */ + width?: number; + /** Content height in pixels */ + height?: number; + /** Video duration in seconds (video only) */ + duration?: number; + /** Video thumbnail URL (video only) */ + thumbnailUrl?: string; + /** Character username for @mention (character only) */ + characterUsername?: string; + /** Character profile picture URL (character only) */ + characterProfileUrl?: string; + /** Character permalink (character only) */ + characterPermalink?: string; + /** Chat response content (chat only) */ + chatResponse?: string; + /** Tool calls made during chat (chat only) */ + toolCalls?: ChatToolCall[]; +} + +// ============================================================================ +// Task Error +// ============================================================================ + +/** + * Task error details interface + * Contains the original error information for debugging + */ +export interface TaskErrorDetails { + /** Original error message from the API or system */ + originalError?: string; + /** API response data (sensitive info filtered) */ + apiResponse?: any; + /** Error occurrence timestamp */ + timestamp?: number; +} + +/** + * Task error interface + * Contains detailed information about task failures + */ +export interface TaskError { + /** Error code for categorization (e.g., 'TIMEOUT', 'NETWORK', 'API_ERROR') */ + code: string; + /** Human-readable error message (user-friendly) */ + message: string; + /** Detailed error information for debugging */ + details?: TaskErrorDetails; +} + +// ============================================================================ +// Task Interface +// ============================================================================ + +/** + * Core task interface + * Represents a single generation task with all its metadata + */ +export interface Task { + /** Unique task identifier (UUID v4) */ + id: string; + /** Type of content to generate */ + type: TaskType; + /** Current task status */ + status: TaskStatus; + /** Parameters for content generation */ + params: GenerationParams; + /** Task creation timestamp (Unix milliseconds) */ + createdAt: number; + /** Last update timestamp (Unix milliseconds) */ + updatedAt: number; + /** Execution start timestamp (Unix milliseconds) */ + startedAt?: number; + /** Completion timestamp (Unix milliseconds) */ + completedAt?: number; + /** Generation result (if successful) */ + result?: TaskResult; + /** Error information (if failed) */ + error?: TaskError; + /** User identifier (reserved for multi-user support) */ + userId?: string; + /** Task progress percentage (0-100) for video generation */ + progress?: number; + /** Remote task ID from API (e.g., videoId for video generation) */ + remoteId?: string; + /** Current execution phase for recovery support */ + executionPhase?: TaskExecutionPhase; + /** Whether the task result has been saved to the media library */ + savedToLibrary?: boolean; + /** Whether the task result has been inserted to canvas */ + insertedToCanvas?: boolean; +} + +// ============================================================================ +// Utility Constants and Functions +// ============================================================================ + +/** + * Terminal task statuses (task lifecycle has ended) + */ +export const TERMINAL_STATUSES: TaskStatus[] = [ + TaskStatus.COMPLETED, + TaskStatus.FAILED, + TaskStatus.CANCELLED, +]; + +/** + * Retryable task statuses (task can be retried) + */ +export const RETRYABLE_STATUSES: TaskStatus[] = [ + TaskStatus.FAILED, + TaskStatus.CANCELLED, +]; + +/** + * Active task statuses (task is currently being processed) + */ +export const ACTIVE_STATUSES: TaskStatus[] = [ + TaskStatus.PENDING, + TaskStatus.PROCESSING, +]; + +/** + * Check if a task status is terminal (lifecycle has ended) + */ +export function isTerminalStatus(status: TaskStatus): boolean { + return TERMINAL_STATUSES.includes(status); +} + +/** + * Check if a task status is retryable + */ +export function isRetryableStatus(status: TaskStatus): boolean { + return RETRYABLE_STATUSES.includes(status); +} + +/** + * Check if a task status is active (currently being processed) + */ +export function isActiveStatus(status: TaskStatus): boolean { + return ACTIVE_STATUSES.includes(status); +} + diff --git a/packages/drawnix/src/types/task.types.ts b/packages/drawnix/src/types/task.types.ts index 4af5c295..e0de2a49 100644 --- a/packages/drawnix/src/types/task.types.ts +++ b/packages/drawnix/src/types/task.types.ts @@ -1,216 +1,22 @@ /** * Task Queue System Type Definitions * - * Defines all TypeScript types and interfaces for the task queue system. - * These types form the foundation for task management, state tracking, and - * integration with the generation API. + * This file re-exports core types from the shared types module. + * The shared types are the single source of truth used by both main thread and SW. + * + * 此文件从共享类型模块重新导出核心类型。 + * 共享类型是主线程和 SW 使用的唯一真相来源。 */ -/** - * Chat tool call interface - * Represents a tool call made during AI chat/analysis - */ -export interface ChatToolCall { - /** Tool name (e.g., 'generate_image', 'generate_video') */ - name: string; - /** Tool arguments */ - arguments: Record; - /** Tool call result */ - result?: { - success: boolean; - taskId?: string; - error?: string; - data?: unknown; - }; -} +// ============================================================================ +// Re-export Core Types from Shared Module +// ============================================================================ -/** - * Task status enumeration - * Represents all possible states a task can be in during its lifecycle - */ -export enum TaskStatus { - /** Task is waiting to be executed */ - PENDING = 'pending', - /** Task is currently being processed */ - PROCESSING = 'processing', - /** Task completed successfully */ - COMPLETED = 'completed', - /** Task failed */ - FAILED = 'failed', - /** Task was cancelled by the user */ - CANCELLED = 'cancelled' -} +// Re-export all types from shared module +export * from './shared/core.types'; -/** - * Task type enumeration - * Defines the types of content that can be generated - */ -export enum TaskType { - /** Image generation task */ - IMAGE = 'image', - /** Video generation task */ - VIDEO = 'video', - /** Character extraction task */ - CHARACTER = 'character', - /** Inspiration board generation task (image + split + layout) */ - INSPIRATION_BOARD = 'inspiration_board', - /** Chat/AI analysis task (text model streaming) */ - CHAT = 'chat', -} - -/** - * Generation parameters interface - * Contains all parameters needed for AI content generation - */ -export interface GenerationParams { - /** Text prompt describing the desired content */ - prompt: string; - /** Image/video width in pixels */ - width?: number; - /** Image/video height in pixels */ - height?: number; - /** Size parameter for API (e.g., '16x9', '1x1') */ - size?: string; - /** Video duration in seconds (video only) */ - duration?: number; - /** Style or model to use for generation */ - style?: string; - /** AI model to use (e.g., 'veo3', 'sora-2') */ - model?: string; - /** Random seed for reproducible generation */ - seed?: number; - /** Source video task ID for character extraction */ - sourceVideoTaskId?: string; - /** Time range for character extraction (format: "start,end") */ - characterTimestamps?: string; - /** Local task ID of source video */ - sourceLocalTaskId?: string; - /** Grid image grid rows (grid_image only) */ - gridImageRows?: number; - /** Grid image grid columns (grid_image only) */ - gridImageCols?: number; - /** Grid image layout style (grid_image only) */ - gridImageLayoutStyle?: 'scattered' | 'grid' | 'circular'; - /** Inspiration board layout style (inspiration_board only) */ - inspirationBoardLayoutStyle?: 'inspiration-board'; - /** Whether this is an inspiration board task */ - isInspirationBoard?: boolean; - /** Inspiration board image count */ - inspirationBoardImageCount?: number; - /** Whether to auto-insert the result to canvas when task completes */ - autoInsertToCanvas?: boolean; - /** Additional parameters for specific generation types */ - [key: string]: any; -} - -/** - * Task result interface - * Contains the output from a successfully completed task - */ -export interface TaskResult { - /** URL to the generated content */ - url: string; - /** File format (e.g., 'png', 'jpg', 'mp4') */ - format: string; - /** File size in bytes */ - size: number; - /** Content width in pixels */ - width?: number; - /** Content height in pixels */ - height?: number; - /** Video duration in seconds (video only) */ - duration?: number; - /** Video thumbnail URL (video only) */ - thumbnailUrl?: string; - /** Character username for @mention (character only) */ - characterUsername?: string; - /** Character profile picture URL (character only) */ - characterProfileUrl?: string; - /** Character permalink (character only) */ - characterPermalink?: string; - /** Chat response content (chat only) */ - chatResponse?: string; - /** Tool calls made during chat (chat only) */ - toolCalls?: ChatToolCall[]; -} - -/** - * Task error details interface - * Contains the original error information for debugging - */ -export interface TaskErrorDetails { - /** Original error message from the API or system */ - originalError?: string; - /** API response data (sensitive info filtered) */ - apiResponse?: any; - /** Error occurrence timestamp */ - timestamp?: number; -} - -/** - * Task error interface - * Contains detailed information about task failures - */ -export interface TaskError { - /** Error code for categorization (e.g., 'TIMEOUT', 'NETWORK', 'API_ERROR') */ - code: string; - /** Human-readable error message (user-friendly) */ - message: string; - /** Detailed error information for debugging */ - details?: TaskErrorDetails; -} - -/** - * Task execution phase enumeration - * Used for tracking async task progress and enabling recovery after page refresh - */ -export enum TaskExecutionPhase { - /** Task is being submitted to the API */ - SUBMITTING = 'submitting', - /** Task submitted, polling for completion (video only) */ - POLLING = 'polling', - /** Task completed, downloading result */ - DOWNLOADING = 'downloading' -} - -/** - * Core task interface - * Represents a single generation task with all its metadata - */ -export interface Task { - /** Unique task identifier (UUID v4) */ - id: string; - /** Type of content to generate */ - type: TaskType; - /** Current task status */ - status: TaskStatus; - /** Parameters for content generation */ - params: GenerationParams; - /** Task creation timestamp (Unix milliseconds) */ - createdAt: number; - /** Last update timestamp (Unix milliseconds) */ - updatedAt: number; - /** Execution start timestamp (Unix milliseconds) */ - startedAt?: number; - /** Completion timestamp (Unix milliseconds) */ - completedAt?: number; - /** Generation result (if successful) */ - result?: TaskResult; - /** Error information (if failed) */ - error?: TaskError; - /** User identifier (reserved for multi-user support) */ - userId?: string; - /** Task progress percentage (0-100) for video generation */ - progress?: number; - /** Remote task ID from API (e.g., videoId for video generation) */ - remoteId?: string; - /** Current execution phase for recovery support */ - executionPhase?: TaskExecutionPhase; - /** Whether the task result has been saved to the media library */ - savedToLibrary?: boolean; - /** Whether the task result has been inserted to canvas */ - insertedToCanvas?: boolean; -} +// Import types for use in this file +import type { Task } from './shared/core.types'; /** * Task queue state interface