Merge branch 'domain' into develop
This commit is contained in:
commit
f14829569f
7 files changed
+1807
-306
No files matched your search
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
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<string>;
|
||||
|
||||
// 恢复工作流
|
||||
recoverWorkflows(boardId: string): Promise<void>;
|
||||
|
||||
// 取消工作流
|
||||
cancelWorkflow(workflowId: string): Promise<void>;
|
||||
|
||||
// 事件流(只读)
|
||||
readonly events$: Observable<WorkflowEvent>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 预期结果
|
||||
|
||||
- **不删除文件** - 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<AppEvent>();
|
||||
|
||||
/**
|
||||
* 发布事件
|
||||
*/
|
||||
export function publishEvent(event: Omit<AppEvent, 'timestamp'>): void {
|
||||
appEvents$.next({
|
||||
...event,
|
||||
timestamp: Date.now(),
|
||||
} as AppEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅所有事件
|
||||
*/
|
||||
export function subscribeToEvents(handler: (event: AppEvent) => void) {
|
||||
return appEvents$.subscribe(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅特定类型的事件
|
||||
*/
|
||||
export function subscribeToEventType<T extends AppEvent['type']>(
|
||||
type: T,
|
||||
handler: (event: Extract<AppEvent, { type: T }>) => void
|
||||
) {
|
||||
return appEvents$.pipe(
|
||||
filter((e): e is Extract<AppEvent, { type: T }> => 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<Task> {
|
||||
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)
|
||||
|
||||
每个阶段独立可测试,可随时停止或调整。
|
||||
@@ -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<Task | null>;
|
||||
findByStatus(status: TaskStatus): Promise<Task[]>;
|
||||
save(task: Task): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 实现(基础设施层)
|
||||
class IndexedDBTaskRepository implements TaskRepository {
|
||||
// 封装 taskStorageReader/Writer
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 执行策略模式(Strategy Pattern)
|
||||
|
||||
统一 SW 和主线程执行器接口:
|
||||
|
||||
```typescript
|
||||
// 执行策略接口
|
||||
interface TaskExecutionStrategy {
|
||||
execute(task: Task): Promise<TaskResult>;
|
||||
cancel(taskId: string): Promise<void>;
|
||||
resume(task: Task): Promise<TaskResult>;
|
||||
}
|
||||
|
||||
// SW 执行策略
|
||||
class SWExecutionStrategy implements TaskExecutionStrategy { }
|
||||
|
||||
// 主线程降级执行策略
|
||||
class MainThreadExecutionStrategy implements TaskExecutionStrategy { }
|
||||
|
||||
// 策略选择器
|
||||
class ExecutionStrategySelector {
|
||||
async getStrategy(): Promise<TaskExecutionStrategy> {
|
||||
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<DomainEvent>();
|
||||
|
||||
publish(event: DomainEvent): void {
|
||||
this.subject.next(event);
|
||||
}
|
||||
|
||||
subscribe<T extends DomainEvent>(
|
||||
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<Task> {
|
||||
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<void> {
|
||||
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 {
|
||||
<<abstract>>
|
||||
+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<DomainEvent>();
|
||||
|
||||
static getInstance(): DomainEventBus;
|
||||
|
||||
// 发布事件
|
||||
publish(event: DomainEvent): void;
|
||||
|
||||
// 按类型订阅
|
||||
subscribe<T extends DomainEvent>(
|
||||
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<TaskExecutionStrategy> {
|
||||
const swAvailable = await this.checkSWAvailability();
|
||||
return swAvailable ? this.swStrategy : this.mainThreadStrategy;
|
||||
}
|
||||
|
||||
private async checkSWAvailability(): Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- 目的:在实现阶段深度探索现有服务的具体实现细节,包括 SW 端的 handlers、存储实现、通信协议等
|
||||
- 预期结果:获取完整的代码上下文,确保新领域层与现有基础设施的正确集成
|
||||
@@ -147,6 +147,9 @@ Service Worker (后台执行)
|
||||
16. **数据安全保护**:破坏性操作(删除、覆盖)前必须安全检查,当前编辑项不可删除,批量删除需确认,不可逆操作需输入确认文字
|
||||
17. **模块循环依赖**:将共享函数(如 `shouldUseSWTaskQueue`)提取到独立模块打破循环;单例构造函数用 `queueMicrotask` 延迟访问其他模块导出
|
||||
18. **配置对象深拷贝**:`getSetting()` 等返回配置对象的方法需返回深拷贝,防止脱敏函数等外部操作修改全局状态
|
||||
19. **避免过度设计**:优先使用简单的 interface + service 模式;只在有明确需求时才添加 Repository、Strategy 等抽象层
|
||||
20. **删除未使用代码**:未使用的"基础设施"代码应删除,不要"为未来预留";遵循 YAGNI 原则
|
||||
21. **重构先问问题**:重构前明确要解决的实际问题;验证方案是否真的解决问题而非增加复杂度
|
||||
|
||||
### Service Worker 规则
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Task | null>;
|
||||
save(task: Task): Promise<void>;
|
||||
}
|
||||
|
||||
// infrastructure/persistence/indexeddb-task-repository.ts
|
||||
class IndexedDBTaskRepository implements TaskRepository {
|
||||
// 只是包装了现有的 taskStorageReader/Writer
|
||||
}
|
||||
|
||||
// infrastructure/execution/execution-strategy.ts
|
||||
interface TaskExecutionStrategy {
|
||||
execute(task: Task): Promise<TaskResult>;
|
||||
}
|
||||
|
||||
// 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<Task> {
|
||||
const task = { /* ... */ };
|
||||
await taskStorageWriter.saveTask(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async executeTask(task: Task): Promise<void> {
|
||||
// 简单的条件判断,不需要 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<T>(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`)
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
/** 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);
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
/** 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
|
||||
|
||||
Reference in new issue
Block a user