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 键

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

等待 AI 生成完成

-

等待 AI 生成完成

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

Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成

-

生成时间: 2026/1/24 12:59:05

-
- - \ 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
-
-

模板的提示词会自动填充到输入框

-

模板的提示词会自动填充到输入框

- -
-
- -
-
-
- -
-

Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成

-

生成时间: 2026/1/24 12:59:05

-
- - \ 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
-
-

松开鼠标完成绘制

-

松开鼠标完成绘制

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

Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成

-

生成时间: 2026/1/24 12:59:05

-
- - \ 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
-
-

形状将自动创建,可以拖动调整大小

-

形状将自动创建,可以拖动调整大小

- -
-
- -
-
-
- -
-

Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成

-

生成时间: 2026/1/24 12:59:05

-
- - \ 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
-
-

点击画布空白处完成编辑

-

点击画布空白处完成编辑

- -
-
- -
-
-
- -
-

Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成

-

生成时间: 2026/1/24 12:59:05

-
- - \ 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 {