diff --git a/CLAUDE.md b/CLAUDE.md
index 24d901cc..06729a7f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -151,6 +151,7 @@ Service Worker (后台执行)
4. 更新后禁止自动刷新页面,需用户确认
5. SW 枚举值使用小写(`'completed'`、`'image'`、`'video'`),读取 SW 数据时注意匹配
6. 无效配置下创建的任务不应在后续被执行,首次初始化时应清除"孤儿任务"
+7. 跨层数据转换必须传递所有字段,特别是 `options`、`metadata` 等可选字段,遗漏会导致功能静默失败
### React 规则
diff --git a/apps/web/index.html b/apps/web/index.html
index 7efe6e80..846f92d1 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -107,7 +107,7 @@
-
+
= 1000) return 'slow';
return 'normal';
}
+
+/**
+ * Format and syntax highlight JSON string
+ * @param {string} jsonStr - JSON string to format
+ * @returns {string} - HTML with syntax highlighting
+ */
+export function formatJsonWithHighlight(jsonStr) {
+ if (!jsonStr) return '';
+
+ try {
+ // Try to parse and re-stringify with indentation
+ const parsed = JSON.parse(jsonStr);
+ const formatted = JSON.stringify(parsed, null, 2);
+
+ // Apply syntax highlighting
+ return escapeHtml(formatted)
+ .replace(/"([^"]+)":/g, '"$1":')
+ .replace(/: "([^"]*)"/g, ': "$1"')
+ .replace(/: (\d+\.?\d*)/g, ': $1')
+ .replace(/: (true|false)/g, ': $1')
+ .replace(/: (null)/g, ': $1');
+ } catch {
+ // If not valid JSON, just escape and return
+ return escapeHtml(jsonStr);
+ }
+}
+
+/**
+ * Parse request body and extract key parameters for quick preview
+ * @param {string} requestBody - JSON string of request body
+ * @returns {object} - Extracted parameters { model, size, response_format, etc. }
+ */
+export function extractRequestParams(requestBody) {
+ if (!requestBody) return {};
+
+ try {
+ const parsed = JSON.parse(requestBody);
+ return {
+ model: parsed.model,
+ size: parsed.size,
+ response_format: parsed.response_format,
+ seconds: parsed.seconds,
+ n: parsed.n,
+ };
+ } catch {
+ return {};
+ }
+}
diff --git a/apps/web/public/sw-debug/llmapi-logs.js b/apps/web/public/sw-debug/llmapi-logs.js
index f5bffcf6..745370ff 100644
--- a/apps/web/public/sw-debug/llmapi-logs.js
+++ b/apps/web/public/sw-debug/llmapi-logs.js
@@ -4,7 +4,7 @@
*/
import { state, elements } from './state.js';
-import { escapeHtml, formatBytes } from './common.js';
+import { escapeHtml, formatBytes, formatJsonWithHighlight, extractRequestParams } from './common.js';
import { downloadJson } from './utils.js';
import { showToast } from './toast.js';
@@ -138,6 +138,9 @@ function createLLMApiEntry(log, isExpanded, onToggle) {
// Show full prompt in header (will wrap if needed)
const promptPreview = log.prompt || '-';
+
+ // Extract request parameters from requestBody
+ const reqParams = extractRequestParams(log.requestBody);
// Render reference images preview
let referenceImagesHtml = '';
@@ -207,6 +210,24 @@ function createLLMApiEntry(log, isExpanded, onToggle) {
耗时 |
${durationText} |
+ ${reqParams.size ? `
+
+ | 尺寸 |
+ ${reqParams.size} |
+
+ ` : ''}
+ ${reqParams.response_format ? `
+
+ | 响应格式 |
+ ${reqParams.response_format} |
+
+ ` : ''}
+ ${reqParams.seconds ? `
+
+ | 时长 |
+ ${reqParams.seconds}s |
+
+ ` : ''}
${log.hasReferenceImages ? `
| 参考图 |
@@ -256,8 +277,8 @@ function createLLMApiEntry(log, isExpanded, onToggle) {
` : ''}
${log.requestBody ? `
-
请求体 (Request Body)
-
${escapeHtml(log.requestBody)}
+
请求参数 (Request Parameters)
+
${formatJsonWithHighlight(log.requestBody)}
` : ''}
${log.resultText ? `
diff --git a/apps/web/public/sw-debug/styles.css b/apps/web/public/sw-debug/styles.css
index d434fe14..c5070767 100644
--- a/apps/web/public/sw-debug/styles.css
+++ b/apps/web/public/sw-debug/styles.css
@@ -2401,3 +2401,59 @@ body.analysis-mode .log-entry {
text-align: center;
font-variant-numeric: tabular-nums;
}
+
+/* JSON Syntax Highlighting */
+.json-highlight {
+ background: var(--bg-tertiary);
+ padding: 12px;
+ border-radius: 6px;
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
+ font-size: 12px;
+ line-height: 1.5;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.json-key {
+ color: #9cdcfe;
+}
+
+.json-string {
+ color: #ce9178;
+}
+
+.json-number {
+ color: #b5cea8;
+}
+
+.json-boolean {
+ color: #569cd6;
+}
+
+.json-null {
+ color: #569cd6;
+}
+
+/* Light mode JSON highlighting */
+@media (prefers-color-scheme: light) {
+ .json-key {
+ color: #0451a5;
+ }
+
+ .json-string {
+ color: #a31515;
+ }
+
+ .json-number {
+ color: #098658;
+ }
+
+ .json-boolean {
+ color: #0000ff;
+ }
+
+ .json-null {
+ color: #0000ff;
+ }
+}
diff --git a/apps/web/public/version.json b/apps/web/public/version.json
index 7959e481..e0be75f8 100644
--- a/apps/web/public/version.json
+++ b/apps/web/public/version.json
@@ -1,6 +1,6 @@
{
"version": "0.5.51",
- "buildTime": "2026-01-25T13:53:49.739Z",
+ "buildTime": "2026-01-25T14:54:21.596Z",
"gitCommit": "unknown",
"changelog": [
"add alignment, distribution, and boolean operation buttons",
diff --git a/apps/web/src/sw/task-queue/handlers/character.ts b/apps/web/src/sw/task-queue/handlers/character.ts
index c89a8f1e..5ef1497c 100644
--- a/apps/web/src/sw/task-queue/handlers/character.ts
+++ b/apps/web/src/sw/task-queue/handlers/character.ts
@@ -150,12 +150,20 @@ export class CharacterHandler implements TaskHandler {
const { debugFetch } = await import('../debug-fetch');
const { startLLMApiLog, completeLLMApiLog, failLLMApiLog } = await import('../llm-api-logger');
+ // 构建请求参数的日志表示
+ const requestParamsForLog = {
+ character_from_task: params.sourceVideoTaskId || '',
+ model: characterModel,
+ ...(params.characterTimestamps && { character_timestamps: params.characterTimestamps }),
+ };
+
const startTime = Date.now();
const logId = startLLMApiLog({
endpoint: '/videos',
model: characterModel,
taskType: 'character',
prompt: `角色提取: ${params.sourceVideoTaskId}`,
+ requestBody: JSON.stringify(requestParamsForLog, null, 2),
taskId: task.id,
});
diff --git a/apps/web/src/sw/task-queue/handlers/image.ts b/apps/web/src/sw/task-queue/handlers/image.ts
index 4eaa7ea9..fa6da995 100644
--- a/apps/web/src/sw/task-queue/handlers/image.ts
+++ b/apps/web/src/sw/task-queue/handlers/image.ts
@@ -131,11 +131,22 @@ export class ImageHandler implements TaskHandler {
config.onProgress(task.id, 10, TaskExecutionPhase.SUBMITTING);
const startTime = Date.now();
+
+ // 为日志记录构建完整的请求体(不包含参考图片的 base64 数据以节省空间)
+ const requestBodyForLog = {
+ ...requestBody,
+ // 如果有参考图片,只记录数量而不记录 base64 数据
+ ...(processedRefImages && processedRefImages.length > 0
+ ? { reference_images: `[${processedRefImages.length} images - data omitted]` }
+ : {}),
+ };
+
const logId = startLLMApiLog({
endpoint: '/images/generations',
model: geminiConfig.modelName || 'unknown',
taskType: 'image',
prompt: params.prompt as string,
+ requestBody: JSON.stringify(requestBodyForLog, null, 2),
hasReferenceImages: !!processedRefImages && processedRefImages.length > 0,
referenceImageCount: processedRefImages?.length,
referenceImages: referenceImageInfos,
diff --git a/apps/web/src/sw/task-queue/handlers/video.ts b/apps/web/src/sw/task-queue/handlers/video.ts
index 468f55b2..e665831d 100644
--- a/apps/web/src/sw/task-queue/handlers/video.ts
+++ b/apps/web/src/sw/task-queue/handlers/video.ts
@@ -214,11 +214,22 @@ export class VideoHandler implements TaskHandler {
const startTime = Date.now();
const model = (params.model as string) || 'veo3';
+
+ // 构建请求参数的日志表示(FormData 无法直接序列化)
+ const requestParamsForLog = {
+ model,
+ prompt: params.prompt,
+ ...(params.duration && { seconds: params.duration }),
+ ...(params.size && { size: params.size }),
+ ...(refUrls.length > 0 && { input_reference: `[${refUrls.length} images]` }),
+ };
+
const logId = startLLMApiLog({
endpoint: '/videos',
model,
taskType: 'video',
prompt: params.prompt as string,
+ requestBody: JSON.stringify(requestParamsForLog, null, 2),
hasReferenceImages: refUrls.length > 0,
referenceImageCount: refUrls.length,
referenceImages: referenceImageInfos,
diff --git a/apps/web/src/sw/task-queue/llm-api-logger.ts b/apps/web/src/sw/task-queue/llm-api-logger.ts
index 22ed03d2..4d31d3a7 100644
--- a/apps/web/src/sw/task-queue/llm-api-logger.ts
+++ b/apps/web/src/sw/task-queue/llm-api-logger.ts
@@ -26,8 +26,8 @@ export interface LLMApiLog {
taskType: 'image' | 'video' | 'chat' | 'character' | 'other';
// 请求参数(脱敏后)
- prompt?: string; // 提示词(截断)
- requestBody?: string; // 完整请求体(仅 chat 类型,不截断)
+ prompt?: string; // 提示词
+ requestBody?: string; // 完整请求体(JSON 格式,用于调试)
hasReferenceImages?: boolean; // 是否有参考图
referenceImageCount?: number; // 参考图数量
referenceImages?: LLMReferenceImage[]; // 参考图详情
@@ -299,7 +299,7 @@ export function startLLMApiLog(params: {
model: string;
taskType: LLMApiLog['taskType'];
prompt?: string;
- requestBody?: string; // 完整请求体(仅 chat 类型使用,不截断)
+ requestBody?: string; // 完整请求体(JSON 格式,用于调试)
hasReferenceImages?: boolean;
referenceImageCount?: number;
referenceImages?: LLMReferenceImage[];
@@ -437,11 +437,12 @@ export function failLLMApiLog(
}
/**
- * 截断提示词(保护隐私,减少存储)
+ * 截断提示词(减少存储)
+ * 增加到 2000 字符以便调试时查看完整 prompt
*/
function truncatePrompt(prompt: string): string {
- if (prompt.length <= 200) return prompt;
- return prompt.substring(0, 200) + '...';
+ if (prompt.length <= 2000) return prompt;
+ return prompt.substring(0, 2000) + '...';
}
/**
diff --git a/apps/web/src/sw/task-queue/queue.ts b/apps/web/src/sw/task-queue/queue.ts
index 9348a0f6..8fdda02e 100644
--- a/apps/web/src/sw/task-queue/queue.ts
+++ b/apps/web/src/sw/task-queue/queue.ts
@@ -198,7 +198,7 @@ export class SWTaskQueue {
return true;
}
- // Resume pending tasks
+ // Resume legacy PENDING tasks (deprecated: new tasks start as PROCESSING)
if (task.status === TaskStatus.PENDING) {
return true;
}
@@ -290,8 +290,26 @@ export class SWTaskQueue {
this.executeResume(task, task.remoteId);
} else if (task.status === TaskStatus.PENDING) {
- // Re-execute pending tasks
- this.processQueue();
+ // Handle legacy PENDING tasks - convert to PROCESSING and execute directly
+ const now = Date.now();
+ task.status = TaskStatus.PROCESSING;
+ task.startedAt = now;
+ task.updatedAt = now;
+ task.executionPhase = TaskExecutionPhase.SUBMITTING;
+ this.tasks.set(task.id, task);
+ this.runningTasks.add(task.id);
+
+ await taskQueueStorage.saveTask(task);
+
+ this.broadcastToClients({
+ type: 'TASK_STATUS',
+ taskId: task.id,
+ status: task.status,
+ phase: task.executionPhase,
+ updatedAt: task.updatedAt,
+ });
+
+ this.executeTaskInternal(task);
} else if (
// Video/Character task in processing but no remoteId - try to recover from LLM API logs
(task.type === TaskType.VIDEO || task.type === TaskType.CHARACTER) &&
@@ -394,18 +412,23 @@ export class SWTaskQueue {
await this.storageRestorePromise;
// If this is first-time initialization (no saved config before),
- // clear all PENDING tasks created without valid API key
+ // clear orphan tasks created without valid API key:
+ // - PENDING tasks (legacy)
+ // - PROCESSING tasks without remoteId (never actually started execution)
if (!this.hadSavedConfig) {
- const pendingTasksToRemove: string[] = [];
+ const orphanTasksToRemove: string[] = [];
for (const task of this.tasks.values()) {
- if (task.status === TaskStatus.PENDING) {
- pendingTasksToRemove.push(task.id);
+ const isOrphan =
+ task.status === TaskStatus.PENDING ||
+ (task.status === TaskStatus.PROCESSING && !task.remoteId && !this.runningTasks.has(task.id));
+ if (isOrphan) {
+ orphanTasksToRemove.push(task.id);
}
}
- if (pendingTasksToRemove.length > 0) {
- console.log(`[SWTaskQueue] First-time init: clearing ${pendingTasksToRemove.length} orphan PENDING tasks created without API key`);
- for (const taskId of pendingTasksToRemove) {
+ if (orphanTasksToRemove.length > 0) {
+ console.log(`[SWTaskQueue] First-time init: clearing ${orphanTasksToRemove.length} orphan tasks created without API key`);
+ for (const taskId of orphanTasksToRemove) {
this.tasks.delete(taskId);
await taskQueueStorage.deleteTask(taskId);
}
@@ -484,25 +507,29 @@ export class SWTaskQueue {
// Check for similar task with same prompt that's already processing or pending
// This prevents duplicate submissions after page refresh
- const existingTask = Array.from(this.tasks.values()).find(
- t => (t.status === TaskStatus.PROCESSING || t.status === TaskStatus.PENDING) &&
- t.type === taskType &&
- t.params.prompt === params.prompt
- );
- if (existingTask) {
- console.warn(
- `[SWTaskQueue] Similar task ${existingTask.id} with same prompt already ${existingTask.status}, skipping duplicate`
+ // NOTE: Skip this check for batch generation (tasks with batchId)
+ // batchId indicates intentional batch generation, not accidental duplicate submission
+ if (!params.batchId) {
+ const existingTask = Array.from(this.tasks.values()).find(
+ t => (t.status === TaskStatus.PROCESSING || t.status === TaskStatus.PENDING) &&
+ t.type === taskType &&
+ t.params.prompt === params.prompt
);
- // Broadcast the existing task's status to sync the client
- this.broadcastToClients({
- type: 'TASK_STATUS',
- taskId: existingTask.id,
- status: existingTask.status,
- progress: existingTask.progress,
- phase: existingTask.executionPhase,
- updatedAt: existingTask.updatedAt,
- });
- return;
+ if (existingTask) {
+ console.warn(
+ `[SWTaskQueue] Similar task ${existingTask.id} with same prompt already ${existingTask.status}, skipping duplicate`
+ );
+ // Broadcast the existing task's status to sync the client
+ this.broadcastToClients({
+ type: 'TASK_STATUS',
+ taskId: existingTask.id,
+ status: existingTask.status,
+ progress: existingTask.progress,
+ phase: existingTask.executionPhase,
+ updatedAt: existingTask.updatedAt,
+ });
+ return;
+ }
}
// Debug logging for task submission
@@ -514,13 +541,16 @@ export class SWTaskQueue {
const task: SWTask = {
id: taskId,
type: taskType,
- status: TaskStatus.PENDING,
+ status: TaskStatus.PROCESSING,
params,
createdAt: now,
updatedAt: now,
+ startedAt: now,
+ executionPhase: TaskExecutionPhase.SUBMITTING,
};
this.tasks.set(taskId, task);
+ this.runningTasks.add(taskId);
// Persist to IndexedDB
await taskQueueStorage.saveTask(task);
@@ -531,8 +561,8 @@ export class SWTaskQueue {
task,
});
- // console.log(`[SWTaskQueue] Task submitted: ${taskId}`);
- this.processQueue();
+ // Execute task immediately (no PENDING state, direct execution)
+ this.executeTaskInternal(task);
}
/**
@@ -585,10 +615,14 @@ export class SWTaskQueue {
}
}
- task.status = TaskStatus.PENDING;
+ const now = Date.now();
+ task.status = TaskStatus.PROCESSING;
task.error = undefined;
- task.updatedAt = Date.now();
+ task.startedAt = now;
+ task.updatedAt = now;
+ task.executionPhase = TaskExecutionPhase.SUBMITTING;
this.tasks.set(taskId, task);
+ this.runningTasks.add(taskId);
// Persist
await taskQueueStorage.saveTask(task);
@@ -598,10 +632,12 @@ export class SWTaskQueue {
type: 'TASK_STATUS',
taskId: task.id,
status: task.status,
+ phase: task.executionPhase,
updatedAt: task.updatedAt,
});
- this.processQueue();
+ // Execute task immediately (no PENDING state, direct execution)
+ this.executeTaskInternal(task);
}
/**
@@ -856,7 +892,9 @@ export class SWTaskQueue {
// ============================================================================
/**
- * Process the task queue
+ * Process the task queue (legacy compatibility only)
+ * Note: New tasks are created with PROCESSING status and executed immediately.
+ * This method only handles legacy PENDING tasks from older versions.
*/
private processQueue(): void {
if (!this.initialized) {
@@ -864,7 +902,7 @@ export class SWTaskQueue {
return;
}
- // Get pending tasks that are not already running
+ // Get legacy pending tasks that are not already running
const pendingTasks = Array.from(this.tasks.values())
.filter((t) => t.status === TaskStatus.PENDING && !this.runningTasks.has(t.id))
.sort((a, b) => a.createdAt - b.createdAt);
@@ -876,7 +914,8 @@ export class SWTaskQueue {
}
/**
- * Execute a single task
+ * Execute a single task (called from processQueue for legacy PENDING tasks)
+ * For new tasks, use executeTaskInternal directly after setting up status
*/
private async executeTask(task: SWTask): Promise {
if (!this.geminiConfig || !this.videoConfig) {
@@ -890,20 +929,28 @@ export class SWTaskQueue {
return;
}
- // Also check if task status has changed (might have been executed by another call)
+ // Check if task is ready for execution (PENDING or PROCESSING without being in runningTasks)
const currentTask = this.tasks.get(task.id);
- if (currentTask && currentTask.status !== TaskStatus.PENDING) {
- console.warn(`[SWTaskQueue] Task ${task.id} is no longer PENDING (${currentTask.status}), skipping`);
+ if (!currentTask) {
+ console.warn(`[SWTaskQueue] Task ${task.id} not found, skipping`);
+ return;
+ }
+
+ // Skip if task is in terminal state
+ if (currentTask.status === TaskStatus.COMPLETED ||
+ currentTask.status === TaskStatus.FAILED ||
+ currentTask.status === TaskStatus.CANCELLED) {
+ console.warn(`[SWTaskQueue] Task ${task.id} is in terminal state (${currentTask.status}), skipping`);
return;
}
this.runningTasks.add(task.id);
- // Update status to processing
+ // Update status to processing (handles legacy PENDING tasks)
task.status = TaskStatus.PROCESSING;
- task.startedAt = Date.now();
+ task.startedAt = task.startedAt || Date.now();
task.updatedAt = Date.now();
- task.executionPhase = TaskExecutionPhase.SUBMITTING;
+ task.executionPhase = task.executionPhase || TaskExecutionPhase.SUBMITTING;
this.tasks.set(task.id, task);
// Persist status change
@@ -917,9 +964,23 @@ export class SWTaskQueue {
updatedAt: task.updatedAt,
});
+ // Execute the task
+ await this.executeTaskInternal(task);
+ }
+
+ /**
+ * Internal task execution - assumes task is already set to PROCESSING and in runningTasks
+ */
+ private async executeTaskInternal(task: SWTask): Promise {
+ if (!this.geminiConfig || !this.videoConfig) {
+ console.warn('[SWTaskQueue] Config not set, cannot execute task:', task.id);
+ return;
+ }
+
// 如果任务已经有 remoteId(视频/角色),则直接进入恢复流程,跳过提交阶段,只重试获取进度的接口
if (task.remoteId && (task.type === TaskType.VIDEO || task.type === TaskType.CHARACTER)) {
task.executionPhase = TaskExecutionPhase.POLLING;
+ task.updatedAt = Date.now();
// 更新存储
await taskQueueStorage.saveTask(task);
diff --git a/apps/web/src/sw/task-queue/utils/media-generation-utils.ts b/apps/web/src/sw/task-queue/utils/media-generation-utils.ts
index 1382e9b0..1ee061ad 100644
--- a/apps/web/src/sw/task-queue/utils/media-generation-utils.ts
+++ b/apps/web/src/sw/task-queue/utils/media-generation-utils.ts
@@ -1278,6 +1278,8 @@ export async function parseImageGenerationResponse(data: any, taskId?: string):
// 检查是否包含违禁内容错误
if (imageData.revised_prompt?.includes('PROHIBITED_CONTENT')) {
throw new Error('内容被拒绝:包含违禁内容');
+ } else if (imageData.revised_prompt?.includes('NO_IMAGE')) {
+ throw new Error('该模型为多模态模型,未生成图片,可更换提示词明确生成图片试试');
}
throw new Error('No image URL in response');
}
diff --git a/apps/web/src/sw/task-queue/workflow-executor.ts b/apps/web/src/sw/task-queue/workflow-executor.ts
index 8128649c..cdf7781a 100644
--- a/apps/web/src/sw/task-queue/workflow-executor.ts
+++ b/apps/web/src/sw/task-queue/workflow-executor.ts
@@ -762,11 +762,19 @@ export class WorkflowExecutor {
// Check if this tool needs to run in main thread
if (requiresMainThread(step.mcp) || !getSWMCPTool(step.mcp)) {
// Delegate to main thread
+ // Merge batch options into args for main thread (batchId, batchIndex, batchTotal)
+ const argsWithOptions = {
+ ...step.args,
+ ...(step.options?.batchId && { batchId: step.options.batchId }),
+ ...(step.options?.batchIndex && { batchIndex: step.options.batchIndex }),
+ ...(step.options?.batchTotal && { batchTotal: step.options.batchTotal }),
+ ...(step.options?.globalIndex && { globalIndex: step.options.globalIndex }),
+ };
const response = await this.requestMainThreadTool(
workflow.id,
step.id,
step.mcp,
- step.args
+ argsWithOptions
);
if (!response.success) {
diff --git a/apps/web/src/sw/task-queue/workflow-types.ts b/apps/web/src/sw/task-queue/workflow-types.ts
index 3adcfc9c..c884db4a 100644
--- a/apps/web/src/sw/task-queue/workflow-types.ts
+++ b/apps/web/src/sw/task-queue/workflow-types.ts
@@ -16,6 +16,22 @@ import type { TaskStatus, TaskExecutionPhase, GeminiConfig, VideoAPIConfig } fro
*/
export type WorkflowStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
+/**
+ * Workflow step execution options (batch generation etc.)
+ */
+export interface WorkflowStepOptions {
+ /** Execution mode */
+ mode?: 'async' | 'queue';
+ /** Batch ID for batch generation */
+ batchId?: string;
+ /** Batch index (1-based) */
+ batchIndex?: number;
+ /** Total batch count */
+ batchTotal?: number;
+ /** Global index */
+ globalIndex?: number;
+}
+
/**
* A single step in a workflow
*/
@@ -38,6 +54,8 @@ export interface WorkflowStep {
duration?: number;
/** Dependencies - step IDs that must complete before this step */
dependsOn?: string[];
+ /** Execution options (batch info etc.) */
+ options?: WorkflowStepOptions;
}
/**
diff --git a/docs/CODING_RULES.md b/docs/CODING_RULES.md
index f7ddf32b..4a417020 100644
--- a/docs/CODING_RULES.md
+++ b/docs/CODING_RULES.md
@@ -1125,6 +1125,52 @@ async initialize(): Promise {
**原因**: `settingsManager` 使用异步方法 `decryptSensitiveDataForLoading()` 解密敏感数据(如 API Key)。如果在解密完成前调用 `geminiSettings.get()`,会返回加密的 JSON 对象而不是解密后的字符串,导致 API 请求失败。
+### 跨层数据转换必须传递所有字段
+
+**场景**: 在主线程和 Service Worker 之间传递数据对象时,需要将内部类型转换为传输格式
+
+❌ **错误示例**:
+```typescript
+// 错误:转换时漏掉了 options 字段
+const swWorkflow = {
+ id: legacyWorkflow.id,
+ name: legacyWorkflow.name,
+ steps: legacyWorkflow.steps.map(step => ({
+ id: step.id,
+ mcp: step.mcp,
+ args: step.args,
+ description: step.description,
+ status: step.status,
+ // 漏掉了 step.options!导致批量信息(batchId 等)丢失
+ })),
+};
+```
+
+✅ **正确示例**:
+```typescript
+// 正确:显式传递所有字段,包括可选字段
+const swWorkflow = {
+ id: legacyWorkflow.id,
+ name: legacyWorkflow.name,
+ steps: legacyWorkflow.steps.map(step => ({
+ id: step.id,
+ mcp: step.mcp,
+ args: step.args,
+ description: step.description,
+ status: step.status,
+ options: step.options, // 包含 batchId, batchIndex, batchTotal 等
+ })),
+};
+```
+
+**常见遗漏的字段**:
+- `options` - 批量参数、执行模式等
+- `metadata` - 元数据信息
+- `context` - 上下文信息
+- 任何 `?:` 可选字段
+
+**原因**: 跨层通信时,如果源类型有可选字段,在转换时很容易遗漏。这会导致功能静默失败(如批量生成只执行第一个),且很难排查。建议在转换函数中显式列出所有字段,或使用 TypeScript 的类型检查确保字段完整。
+
### Service Worker 初始化时序
**场景**: 提交工作流到 Service Worker 执行前
diff --git a/docs/user-manual/使用-ai-生成图片.html b/docs/user-manual/使用-ai-生成图片.html
deleted file mode 100644
index 14d10418..00000000
--- a/docs/user-manual/使用-ai-生成图片.html
+++ /dev/null
@@ -1,237 +0,0 @@
-
-
-
-
-
- 使用 AI 生成图片 - Aitu 用户手册
-
-
-
-
-
-
- 使用 AI 生成图片
- 学习如何使用 AI 功能生成图片
-
-
-
-
-
1
-
-
在底部输入框中输入图片描述
-
在底部输入框中输入图片描述
-

-
-
-
-
-
2
-
-
点击 # 选择生成模型
-
点击 # 选择生成模型
-

-
-
-
-
-
3
-
-
点击发送按钮或按 Enter 键
-
点击发送按钮或按 Enter 键
-

-
-
-
-
-
4
-
-
等待 AI 生成完成
-
等待 AI 生成完成
-

-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/user-manual/使用灵感创意板.html b/docs/user-manual/使用灵感创意板.html
deleted file mode 100644
index 0e4f9d4d..00000000
--- a/docs/user-manual/使用灵感创意板.html
+++ /dev/null
@@ -1,237 +0,0 @@
-
-
-
-
-
- 使用灵感创意板 - Aitu 用户手册
-
-
-
-
-
-
- 使用灵感创意板
- 当画布为空时,灵感创意板会显示推荐的创作模板
-
-
-
-
-
1
-
-
在空画布上,灵感创意板自动显示
-
在空画布上,灵感创意板自动显示
-

-
-
-
-
-
2
-
-
浏览不同的创意模板
-
浏览不同的创意模板
-

-
-
-
-
-
3
-
-
点击感兴趣的模板
-
点击感兴趣的模板
-
-
-
-
-
-
4
-
-
模板的提示词会自动填充到输入框
-
模板的提示词会自动填充到输入框
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/user-manual/使用画笔工具绘制.html b/docs/user-manual/使用画笔工具绘制.html
deleted file mode 100644
index 37f8e165..00000000
--- a/docs/user-manual/使用画笔工具绘制.html
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
- 使用画笔工具 - Aitu 用户手册
-
-
-
-
-
-
- 使用画笔工具
- 学习如何使用画笔工具在画布上自由绘制
-
-
-
-
-
1
-
-
点击左侧工具栏的画笔图标
-
点击左侧工具栏的画笔图标
-

-
-
-
-
-
2
-
-
在画布上按住鼠标左键并拖动
-
在画布上按住鼠标左键并拖动
-

-
-
-
-
-
3
-
-
松开鼠标完成绘制
-
松开鼠标完成绘制
-

-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/user-manual/创建形状.html b/docs/user-manual/创建形状.html
deleted file mode 100644
index 054aa855..00000000
--- a/docs/user-manual/创建形状.html
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
- 创建形状 - Aitu 用户手册
-
-
-
-
-
-
- 创建形状
- 学习如何在画布上创建各种形状
-
-
-
-
-
1
-
-
点击左侧工具栏的形状图标
-
点击左侧工具栏的形状图标
-

-
-
-
-
-
2
-
-
在画布上点击要放置形状的位置
-
在画布上点击要放置形状的位置
-

-
-
-
-
-
3
-
-
形状将自动创建,可以拖动调整大小
-
形状将自动创建,可以拖动调整大小
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/user-manual/添加文本.html b/docs/user-manual/添加文本.html
deleted file mode 100644
index eb8fa783..00000000
--- a/docs/user-manual/添加文本.html
+++ /dev/null
@@ -1,237 +0,0 @@
-
-
-
-
-
- 添加文本 - Aitu 用户手册
-
-
-
-
-
-
- 添加文本
- 学习如何在画布上添加文本
-
-
-
-
-
1
-
-
点击左侧工具栏的文本图标
-
点击左侧工具栏的文本图标
-

-
-
-
-
-
2
-
-
在画布上点击要添加文本的位置
-
在画布上点击要添加文本的位置
-

-
-
-
-
-
3
-
-
输入您想要的文字内容
-
输入您想要的文字内容
-
-
-
-
-
-
4
-
-
点击画布空白处完成编辑
-
点击画布空白处完成编辑
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/package.json b/package.json
index ca4277e2..0c18c0a3 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"version:patch": "node scripts/safe-version-bump.js patch",
"version:minor": "node scripts/safe-version-bump.js minor",
"version:major": "node scripts/safe-version-bump.js major",
- "release:skip": "node scripts/deploy-hybrid.js --skip-npm",
+ "release:skip": "node scripts/deploy-hybrid.js --skip-build --skip-e2e --skip-npm --skip-manual",
"release": "npm run version:patch && node scripts/deploy-hybrid.js --skip-e2e",
"release:minor": "npm run version:minor && node scripts/deploy-hybrid.js",
"release:major": "npm run version:major && node scripts/deploy-hybrid.js",
diff --git a/packages/drawnix/src/components/task-queue/DialogTaskList.tsx b/packages/drawnix/src/components/task-queue/DialogTaskList.tsx
index 2bd11909..66089150 100644
--- a/packages/drawnix/src/components/task-queue/DialogTaskList.tsx
+++ b/packages/drawnix/src/components/task-queue/DialogTaskList.tsx
@@ -57,8 +57,9 @@ export const DialogTaskList: React.FC = ({
const taskMatchesQuery = (task: any, query: string) => {
if (!query.trim()) return true;
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
+ // Note: PENDING is deprecated, displayed as '处理中' for legacy compatibility
const statusLabelMap: Record = {
- [TaskStatus.PENDING]: '待处理',
+ [TaskStatus.PENDING]: '处理中',
[TaskStatus.PROCESSING]: '处理中',
[TaskStatus.COMPLETED]: '已完成',
[TaskStatus.FAILED]: '失败',
diff --git a/packages/drawnix/src/components/task-queue/TaskItem.tsx b/packages/drawnix/src/components/task-queue/TaskItem.tsx
index a7e39010..977048e7 100644
--- a/packages/drawnix/src/components/task-queue/TaskItem.tsx
+++ b/packages/drawnix/src/components/task-queue/TaskItem.tsx
@@ -50,11 +50,11 @@ export interface TaskItemProps {
/**
* Gets the appropriate status tag color based on task status
+ * Note: PENDING is deprecated, treated same as PROCESSING for legacy compatibility
*/
function getStatusTagTheme(status: TaskStatus): 'default' | 'primary' | 'success' | 'warning' | 'danger' {
switch (status) {
case TaskStatus.PENDING:
- return 'default';
case TaskStatus.PROCESSING:
return 'primary';
case TaskStatus.COMPLETED:
@@ -70,11 +70,11 @@ function getStatusTagTheme(status: TaskStatus): 'default' | 'primary' | 'success
/**
* Gets the status label in Chinese
+ * Note: PENDING is deprecated, displayed as '处理中' for legacy compatibility
*/
function getStatusLabel(status: TaskStatus): string {
switch (status) {
case TaskStatus.PENDING:
- return '待处理';
case TaskStatus.PROCESSING:
return '处理中';
case TaskStatus.COMPLETED:
diff --git a/packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx b/packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
index b57083c0..8608b680 100644
--- a/packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
+++ b/packages/drawnix/src/components/ttd-dialog/ai-image-generation.tsx
@@ -233,7 +233,7 @@ const AIImageGeneration = ({
};
const handleGenerate = async (count: number = 1) => {
- if (!prompt.trim()) {
+ if (!prompt || !prompt.trim()) {
setError(
language === 'zh' ? '请输入图像描述' : 'Please enter image description'
);
@@ -274,7 +274,7 @@ const AIImageGeneration = ({
for (let i = 0; i < count; i++) {
const taskParams = {
- prompt: prompt.trim(),
+ prompt: (prompt || '').trim(),
width: finalWidth,
height: finalHeight,
aspectRatio,
@@ -323,7 +323,7 @@ const AIImageGeneration = ({
// 创建任务参数
const taskParams = {
- prompt: prompt.trim(),
+ prompt: (prompt || '').trim(),
width: finalWidth,
height: finalHeight,
aspectRatio,
@@ -449,7 +449,7 @@ const AIImageGeneration = ({
type="image"
isGenerating={isGenerating}
hasGenerated={false}
- canGenerate={!!prompt.trim()}
+ canGenerate={!!(prompt && prompt.trim())}
onGenerate={handleGenerate}
onReset={handleReset}
leftContent={
diff --git a/packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx b/packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx
index 8faae498..1dd7995c 100644
--- a/packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx
+++ b/packages/drawnix/src/components/ttd-dialog/ai-video-generation.tsx
@@ -473,7 +473,7 @@ const AIVideoGeneration = ({
}
} else {
// 普通模式验证
- if (!prompt.trim()) {
+ if (!prompt || !prompt.trim()) {
setError(language === 'zh' ? '请输入视频描述' : 'Please enter video description');
return;
}
@@ -507,7 +507,7 @@ const AIVideoGeneration = ({
// 构建最终提示词
const finalPrompt = storyboardEnabled
? formatStoryboardPrompt(storyboardScenes)
- : prompt.trim();
+ : (prompt || '').trim();
// 批量生成逻辑
const batchTaskIds: string[] = [];
@@ -694,7 +694,7 @@ const AIVideoGeneration = ({
type="video"
isGenerating={isGenerating}
hasGenerated={false}
- canGenerate={storyboardEnabled ? storyboardScenes.length > 0 : !!prompt.trim()}
+ canGenerate={storyboardEnabled ? storyboardScenes.length > 0 : !!(prompt && prompt.trim())}
onGenerate={handleGenerate}
onReset={handleReset}
/>
diff --git a/packages/drawnix/src/components/ttd-dialog/shared/PromptInput.tsx b/packages/drawnix/src/components/ttd-dialog/shared/PromptInput.tsx
index 0c08d43b..ab9a8328 100644
--- a/packages/drawnix/src/components/ttd-dialog/shared/PromptInput.tsx
+++ b/packages/drawnix/src/components/ttd-dialog/shared/PromptInput.tsx
@@ -112,8 +112,9 @@ export const PromptInput: React.FC = ({
}
}, [isPresetOpen]);
- // 选择提示词
- const handleSelect = useCallback((content: string) => {
+ // 选择提示词 - 接收 PromptItem 对象或字符串
+ const handleSelect = useCallback((item: PromptItem | string) => {
+ const content = typeof item === 'string' ? item : item.content;
onPromptChange(content);
onError?.(null);
setIsPresetOpen(false);
diff --git a/packages/drawnix/src/hooks/useWorkflowSubmission.ts b/packages/drawnix/src/hooks/useWorkflowSubmission.ts
index 5f976313..d9c7e804 100644
--- a/packages/drawnix/src/hooks/useWorkflowSubmission.ts
+++ b/packages/drawnix/src/hooks/useWorkflowSubmission.ts
@@ -503,6 +503,8 @@ export function useWorkflowSubmission(
args: step.args,
description: step.description,
status: step.status as any,
+ // Include batch options for batch generation
+ options: step.options,
})),
status: 'pending',
createdAt: legacyWorkflow.createdAt,
diff --git a/packages/drawnix/src/mcp/tools/image-generation.ts b/packages/drawnix/src/mcp/tools/image-generation.ts
index 70da208f..a9c7cdae 100644
--- a/packages/drawnix/src/mcp/tools/image-generation.ts
+++ b/packages/drawnix/src/mcp/tools/image-generation.ts
@@ -47,6 +47,14 @@ export interface ImageGenerationParams {
model?: string;
/** 生成数量(仅 queue 模式支持) */
count?: number;
+ /** 批次 ID(批量生成时) */
+ batchId?: string;
+ /** 批次索引(1-based) */
+ batchIndex?: number;
+ /** 批次总数 */
+ batchTotal?: number;
+ /** 全局索引 */
+ globalIndex?: number;
}
/**
@@ -137,7 +145,11 @@ async function executeAsync(params: ImageGenerationParams): Promise {
* 支持批量创建任务(通过 count 参数)
*/
function executeQueue(params: ImageGenerationParams, options: MCPExecuteOptions): MCPTaskResult {
- const { prompt, size, referenceImages, model, count = 1 } = params;
+ const {
+ prompt, size, referenceImages, model, count = 1,
+ // 批量参数(可能从工作流步骤传入)
+ batchId: paramsBatchId, batchIndex: paramsBatchIndex, batchTotal: paramsBatchTotal, globalIndex: paramsGlobalIndex,
+ } = params;
if (!prompt || typeof prompt !== 'string') {
return {
@@ -155,9 +167,12 @@ function executeQueue(params: ImageGenerationParams, options: MCPExecuteOptions)
name: `reference-${index + 1}`,
}));
- // 生成批次 ID(如果有多个任务)
+ // 批量参数:优先使用 params 中的(工作流场景),否则根据 count 生成
const actualCount = Math.min(Math.max(1, count), 10); // 限制 1-10 个
- const batchId = actualCount > 1 ? `batch_${Date.now()}` : options.batchId;
+ const batchId = paramsBatchId || (actualCount > 1 ? `batch_${Date.now()}` : options.batchId);
+ const batchIndex = paramsBatchIndex;
+ const batchTotal = paramsBatchTotal || (actualCount > 1 ? actualCount : undefined);
+ const globalIndex = paramsGlobalIndex || options.globalIndex;
const createdTasks: any[] = [];
@@ -170,8 +185,27 @@ function executeQueue(params: ImageGenerationParams, options: MCPExecuteOptions)
throw new Error(`重试任务不存在: ${options.retryTaskId}`);
}
createdTasks.push(task);
+ } else if (paramsBatchId && paramsBatchIndex) {
+ // 工作流场景:每个步骤创建一个任务,批量信息已从工作流传入
+ const task = taskQueueService.createTask(
+ {
+ prompt,
+ size: size || '1x1',
+ uploadedImages: uploadedImages && uploadedImages.length > 0 ? uploadedImages : undefined,
+ model: model || getCurrentImageModel(),
+ // 使用工作流传入的批量参数
+ batchId,
+ batchIndex,
+ batchTotal,
+ globalIndex,
+ // 自动插入画布
+ autoInsertToCanvas: true,
+ },
+ TaskType.IMAGE
+ );
+ createdTasks.push(task);
} else {
- // 创建任务(支持批量)
+ // 直接调用场景(如弹窗):根据 count 创建多个任务
for (let i = 0; i < actualCount; i++) {
const task = taskQueueService.createTask(
{
@@ -183,7 +217,7 @@ function executeQueue(params: ImageGenerationParams, options: MCPExecuteOptions)
batchId: batchId,
batchIndex: i + 1,
batchTotal: actualCount,
- globalIndex: options.globalIndex ? options.globalIndex + i : i + 1,
+ globalIndex: globalIndex ? globalIndex + i : i + 1,
// 自动插入画布
autoInsertToCanvas: true,
},
diff --git a/packages/drawnix/src/mcp/tools/video-generation.ts b/packages/drawnix/src/mcp/tools/video-generation.ts
index 3a6785fb..e15e5489 100644
--- a/packages/drawnix/src/mcp/tools/video-generation.ts
+++ b/packages/drawnix/src/mcp/tools/video-generation.ts
@@ -63,6 +63,14 @@ export interface VideoGenerationParams {
referenceImages?: string[];
/** 生成数量(仅 queue 模式支持) */
count?: number;
+ /** 批次 ID(批量生成时) */
+ batchId?: string;
+ /** 批次索引(1-based) */
+ batchIndex?: number;
+ /** 批次总数 */
+ batchTotal?: number;
+ /** 全局索引 */
+ globalIndex?: number;
}
/**
@@ -166,7 +174,11 @@ async function executeAsync(params: VideoGenerationParams): Promise {
* 支持批量创建任务(通过 count 参数)
*/
function executeQueue(params: VideoGenerationParams, options: MCPExecuteOptions): MCPTaskResult {
- const { prompt, model = 'veo3', seconds, size, referenceImages, count = 1 } = params;
+ const {
+ prompt, model = 'veo3', seconds, size, referenceImages, count = 1,
+ // 批量参数(可能从工作流步骤传入)
+ batchId: paramsBatchId, batchIndex: paramsBatchIndex, batchTotal: paramsBatchTotal, globalIndex: paramsGlobalIndex,
+ } = params;
if (!prompt || typeof prompt !== 'string') {
return {
@@ -187,9 +199,12 @@ function executeQueue(params: VideoGenerationParams, options: MCPExecuteOptions)
name: `reference-${index + 1}`,
}));
- // 生成批次 ID(如果有多个任务)
+ // 批量参数:优先使用 params 中的(工作流场景),否则根据 count 生成
const actualCount = Math.min(Math.max(1, count), 10); // 限制 1-10 个
- const batchId = actualCount > 1 ? `batch_${Date.now()}` : options.batchId;
+ const batchId = paramsBatchId || (actualCount > 1 ? `batch_${Date.now()}` : options.batchId);
+ const batchIndex = paramsBatchIndex;
+ const batchTotal = paramsBatchTotal || (actualCount > 1 ? actualCount : undefined);
+ const globalIndex = paramsGlobalIndex || options.globalIndex;
const createdTasks: any[] = [];
@@ -202,8 +217,28 @@ function executeQueue(params: VideoGenerationParams, options: MCPExecuteOptions)
throw new Error(`重试任务不存在: ${options.retryTaskId}`);
}
createdTasks.push(task);
+ } else if (paramsBatchId && paramsBatchIndex) {
+ // 工作流场景:每个步骤创建一个任务,批量信息已从工作流传入
+ const task = taskQueueService.createTask(
+ {
+ prompt,
+ size: size || '16x9',
+ duration: parseInt(seconds || modelConfig.defaultDuration, 10),
+ model,
+ uploadedImages: uploadedImages && uploadedImages.length > 0 ? uploadedImages : undefined,
+ // 使用工作流传入的批量参数
+ batchId,
+ batchIndex,
+ batchTotal,
+ globalIndex,
+ // 自动插入画布
+ autoInsertToCanvas: true,
+ },
+ TaskType.VIDEO
+ );
+ createdTasks.push(task);
} else {
- // 创建任务(支持批量)
+ // 直接调用场景(如弹窗):根据 count 创建多个任务
for (let i = 0; i < actualCount; i++) {
const task = taskQueueService.createTask(
{
@@ -216,7 +251,7 @@ function executeQueue(params: VideoGenerationParams, options: MCPExecuteOptions)
batchId: batchId,
batchIndex: i + 1,
batchTotal: actualCount,
- globalIndex: options.globalIndex ? options.globalIndex + i : i + 1,
+ globalIndex: globalIndex ? globalIndex + i : i + 1,
// 自动插入画布
autoInsertToCanvas: true,
},
diff --git a/packages/drawnix/src/services/sw-capabilities/types.ts b/packages/drawnix/src/services/sw-capabilities/types.ts
index 8db81700..78b21c44 100644
--- a/packages/drawnix/src/services/sw-capabilities/types.ts
+++ b/packages/drawnix/src/services/sw-capabilities/types.ts
@@ -172,6 +172,14 @@ export interface ImageGenerationParams {
quality?: '1k' | '2k' | '4k';
model?: string;
count?: number;
+ /** 批次 ID(批量生成时) */
+ batchId?: string;
+ /** 批次索引(1-based) */
+ batchIndex?: number;
+ /** 批次总数 */
+ batchTotal?: number;
+ /** 全局索引 */
+ globalIndex?: number;
}
/**
@@ -184,6 +192,14 @@ export interface VideoGenerationParams {
size?: string;
referenceImages?: string[];
count?: number;
+ /** 批次 ID(批量生成时) */
+ batchId?: string;
+ /** 批次索引(1-based) */
+ batchIndex?: number;
+ /** 批次总数 */
+ batchTotal?: number;
+ /** 全局索引 */
+ globalIndex?: number;
}
/**
diff --git a/packages/drawnix/src/services/sw-task-queue-service.ts b/packages/drawnix/src/services/sw-task-queue-service.ts
index 75b05e1a..d6a2c69e 100644
--- a/packages/drawnix/src/services/sw-task-queue-service.ts
+++ b/packages/drawnix/src/services/sw-task-queue-service.ts
@@ -102,14 +102,17 @@ class SWTaskQueueService {
const sanitizedParams = sanitizeGenerationParams(params);
// Create task locally for immediate UI feedback
+ // Task starts as PROCESSING since it will be executed immediately by SW
const now = Date.now();
const task: Task = {
id: generateTaskId(),
type,
- status: TaskStatus.PENDING,
+ status: TaskStatus.PROCESSING,
params: sanitizedParams,
createdAt: now,
updatedAt: now,
+ startedAt: now,
+ executionPhase: TaskExecutionPhase.SUBMITTING,
...(type === TaskType.VIDEO && { progress: 0 }),
};
@@ -130,6 +133,10 @@ class SWTaskQueueService {
return Array.from(this.tasks.values());
}
+ /**
+ * @deprecated Tasks no longer use PENDING status. New tasks start as PROCESSING.
+ * Kept for backward compatibility with legacy data.
+ */
getPendingTasks(): Task[] {
return this.getAllTasks().filter((t) => t.status === TaskStatus.PENDING);
}
diff --git a/packages/drawnix/src/services/task-queue-service.ts b/packages/drawnix/src/services/task-queue-service.ts
index cebb3d4c..31676131 100644
--- a/packages/drawnix/src/services/task-queue-service.ts
+++ b/packages/drawnix/src/services/task-queue-service.ts
@@ -6,7 +6,7 @@
*/
import { Subject, Observable } from 'rxjs';
-import { Task, TaskStatus, TaskType, TaskEvent, GenerationParams } from '../types/task.types';
+import { Task, TaskStatus, TaskType, TaskEvent, GenerationParams, TaskExecutionPhase } from '../types/task.types';
import { generateTaskId, isTaskActive } from '../utils/task-utils';
import { validateGenerationParams, sanitizeGenerationParams } from '../utils/validation-utils';
@@ -52,15 +52,17 @@ class TaskQueueService {
// Sanitize parameters
const sanitizedParams = sanitizeGenerationParams(params);
- // Create new task
+ // Create new task - starts as PROCESSING since it will be executed immediately
const now = Date.now();
const task: Task = {
id: generateTaskId(),
type,
- status: TaskStatus.PENDING,
+ status: TaskStatus.PROCESSING,
params: sanitizedParams,
createdAt: now,
updatedAt: now,
+ startedAt: now,
+ executionPhase: TaskExecutionPhase.SUBMITTING,
// Initialize progress for video tasks
...(type === TaskType.VIDEO && { progress: 0 }),
};
@@ -213,13 +215,14 @@ class TaskQueueService {
return;
}
- // Reset task for retry - clear timing fields to prevent immediate timeout
- this.updateTaskStatus(taskId, TaskStatus.PENDING, {
+ // Reset task for retry - set to PROCESSING for immediate execution
+ const now = Date.now();
+ this.updateTaskStatus(taskId, TaskStatus.PROCESSING, {
error: undefined,
- startedAt: undefined, // Reset start time so timeout is recalculated
+ startedAt: now, // Set new start time
completedAt: undefined, // Clear completion time
remoteId: undefined, // Clear remote ID for fresh submission
- executionPhase: undefined, // Clear execution phase
+ executionPhase: TaskExecutionPhase.SUBMITTING,
progress: task.type === TaskType.VIDEO ? 0 : undefined, // Reset progress for video
});
diff --git a/packages/drawnix/src/services/workflow-submission-service.ts b/packages/drawnix/src/services/workflow-submission-service.ts
index f83261fe..3cafe120 100644
--- a/packages/drawnix/src/services/workflow-submission-service.ts
+++ b/packages/drawnix/src/services/workflow-submission-service.ts
@@ -27,6 +27,14 @@ import {
export type WorkflowStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
+export interface WorkflowStepOptions {
+ mode?: 'async' | 'queue';
+ batchId?: string;
+ batchIndex?: number;
+ batchTotal?: number;
+ globalIndex?: number;
+}
+
export interface WorkflowStep {
id: string;
mcp: string;
@@ -36,6 +44,7 @@ export interface WorkflowStep {
result?: unknown;
error?: string;
duration?: number;
+ options?: WorkflowStepOptions;
}
export interface WorkflowDefinition {