fix(batch-generation): 修复批量生成只执行第一个任务的问题
批量生成时(通过 AIInputBar 指定数量 n),之前只会执行 1 个任务,其余被跳过为"重复"。 修复包括: - 跳过 batchId 存在时的重复检测逻辑 - 在工作流转换和执行时正确传递批量参数 (batchId, batchIndex, batchTotal) - 添加跨层数据转换的编码规则文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
78acdbf1f7
commit
ebca700560
33 files changed
+486
-1253
No files matched your search
@@ -151,6 +151,7 @@ Service Worker (后台执行)
|
||||
4. 更新后禁止自动刷新页面,需用户确认
|
||||
5. SW 枚举值使用小写(`'completed'`、`'image'`、`'video'`),读取 SW 数据时注意匹配
|
||||
6. 无效配置下创建的任务不应在后续被执行,首次初始化时应清除"孤儿任务"
|
||||
7. 跨层数据转换必须传递所有字段,特别是 `options`、`metadata` 等可选字段,遗漏会导致功能静默失败
|
||||
|
||||
### React 规则
|
||||
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@
|
||||
</script>
|
||||
<!-- base href 由构建工具处理,CDN 模式下使用相对路径 -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="app-version" content="0.5.50" />
|
||||
<meta name="app-version" content="0.5.51" />
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
|
||||
@@ -138,3 +138,51 @@ export function getSpeedClass(duration) {
|
||||
if (duration >= 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, '<span class="json-key">"$1"</span>:')
|
||||
.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
|
||||
.replace(/: (\d+\.?\d*)/g, ': <span class="json-number">$1</span>')
|
||||
.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
|
||||
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
||||
} 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 {};
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
<td class="form-data-name">耗时</td>
|
||||
<td><span class="form-data-value">${durationText}</span></td>
|
||||
</tr>
|
||||
${reqParams.size ? `
|
||||
<tr>
|
||||
<td class="form-data-name">尺寸</td>
|
||||
<td><span class="form-data-value">${reqParams.size}</span></td>
|
||||
</tr>
|
||||
` : ''}
|
||||
${reqParams.response_format ? `
|
||||
<tr>
|
||||
<td class="form-data-name">响应格式</td>
|
||||
<td><span class="form-data-value">${reqParams.response_format}</span></td>
|
||||
</tr>
|
||||
` : ''}
|
||||
${reqParams.seconds ? `
|
||||
<tr>
|
||||
<td class="form-data-name">时长</td>
|
||||
<td><span class="form-data-value">${reqParams.seconds}s</span></td>
|
||||
</tr>
|
||||
` : ''}
|
||||
${log.hasReferenceImages ? `
|
||||
<tr>
|
||||
<td class="form-data-name">参考图</td>
|
||||
@@ -256,8 +277,8 @@ function createLLMApiEntry(log, isExpanded, onToggle) {
|
||||
` : ''}
|
||||
${log.requestBody ? `
|
||||
<div class="detail-section">
|
||||
<h4>请求体 (Request Body)</h4>
|
||||
<pre>${escapeHtml(log.requestBody)}</pre>
|
||||
<h4>请求参数 (Request Parameters)</h4>
|
||||
<pre class="json-highlight">${formatJsonWithHighlight(log.requestBody)}</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
${log.resultText ? `
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1125,6 +1125,52 @@ async initialize(): Promise<boolean> {
|
||||
|
||||
**原因**: `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 执行前
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>使用 AI 生成图片 - Aitu 用户手册</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #F39C12;
|
||||
--secondary-color: #5A4FCF;
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--border-color: #e0e0e0;
|
||||
--code-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header .version {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.sidebar-nav .category {
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 270px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-section h2 {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-section p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.steps {
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-screenshot {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="index.html"><strong>首页</strong></a></li>
|
||||
<li class="category">绘图功能</li>
|
||||
<li><a href="使用画笔工具绘制.html">使用画笔工具</a></li>
|
||||
<li><a href="创建形状.html">创建形状</a></li>
|
||||
<li><a href="添加文本.html">添加文本</a></li>
|
||||
<li class="category">AI 生成</li>
|
||||
<li><a href="使用-ai-生成图片.html">使用 AI 生成图片</a></li>
|
||||
<li><a href="使用灵感创意板.html">使用灵感创意板</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
<article class="page-section">
|
||||
<h2>使用 AI 生成图片</h2>
|
||||
<p>学习如何使用 AI 功能生成图片</p>
|
||||
|
||||
<div class="steps">
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>在底部输入框中输入图片描述</h4>
|
||||
<p>在底部输入框中输入图片描述</p>
|
||||
<img class="step-screenshot" src="screenshots/初始状态-ee00aa3f861c0d28b5eb6b286f451bb9ecb8a936.png" alt="在底部输入框中输入图片描述">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>点击 # 选择生成模型</h4>
|
||||
<p>点击 # 选择生成模型</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-1-输入提示词-61531907dc06887a91661c4a68f025c87e4694e0.png" alt="点击 # 选择生成模型">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>点击发送按钮或按 Enter 键</h4>
|
||||
<p>点击发送按钮或按 Enter 键</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-2-选择模型-f3728aeeadbe90a7bd9e5bdbf4f4ff0c34f99f54.png" alt="点击发送按钮或按 Enter 键">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<h4>等待 AI 生成完成</h4>
|
||||
<p>等待 AI 生成完成</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-3-准备发送-feb981734e01b38cb7e4bb2563870af8d3bcb422.png" alt="等待 AI 生成完成">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成</p>
|
||||
<p>生成时间: 2026/1/24 12:59:05</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,237 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>使用灵感创意板 - Aitu 用户手册</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #F39C12;
|
||||
--secondary-color: #5A4FCF;
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--border-color: #e0e0e0;
|
||||
--code-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header .version {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.sidebar-nav .category {
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 270px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-section h2 {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-section p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.steps {
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-screenshot {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="index.html"><strong>首页</strong></a></li>
|
||||
<li class="category">绘图功能</li>
|
||||
<li><a href="使用画笔工具绘制.html">使用画笔工具</a></li>
|
||||
<li><a href="创建形状.html">创建形状</a></li>
|
||||
<li><a href="添加文本.html">添加文本</a></li>
|
||||
<li class="category">AI 生成</li>
|
||||
<li><a href="使用-ai-生成图片.html">使用 AI 生成图片</a></li>
|
||||
<li><a href="使用灵感创意板.html">使用灵感创意板</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
<article class="page-section">
|
||||
<h2>使用灵感创意板</h2>
|
||||
<p>当画布为空时,灵感创意板会显示推荐的创作模板</p>
|
||||
|
||||
<div class="steps">
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>在空画布上,灵感创意板自动显示</h4>
|
||||
<p>在空画布上,灵感创意板自动显示</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-1-灵感创意板-0ace33f54c0ccddb7a586762e85d2a6ab8e302f5.png" alt="在空画布上,灵感创意板自动显示">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>浏览不同的创意模板</h4>
|
||||
<p>浏览不同的创意模板</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-2-选择模板后-5ed995df265ec4c7602e5d5fa5392354b8729cc1.png" alt="浏览不同的创意模板">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>点击感兴趣的模板</h4>
|
||||
<p>点击感兴趣的模板</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<h4>模板的提示词会自动填充到输入框</h4>
|
||||
<p>模板的提示词会自动填充到输入框</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成</p>
|
||||
<p>生成时间: 2026/1/24 12:59:05</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,228 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>使用画笔工具 - Aitu 用户手册</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #F39C12;
|
||||
--secondary-color: #5A4FCF;
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--border-color: #e0e0e0;
|
||||
--code-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header .version {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.sidebar-nav .category {
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 270px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-section h2 {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-section p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.steps {
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-screenshot {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="index.html"><strong>首页</strong></a></li>
|
||||
<li class="category">绘图功能</li>
|
||||
<li><a href="使用画笔工具绘制.html">使用画笔工具</a></li>
|
||||
<li><a href="创建形状.html">创建形状</a></li>
|
||||
<li><a href="添加文本.html">添加文本</a></li>
|
||||
<li class="category">AI 生成</li>
|
||||
<li><a href="使用-ai-生成图片.html">使用 AI 生成图片</a></li>
|
||||
<li><a href="使用灵感创意板.html">使用灵感创意板</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
<article class="page-section">
|
||||
<h2>使用画笔工具</h2>
|
||||
<p>学习如何使用画笔工具在画布上自由绘制</p>
|
||||
|
||||
<div class="steps">
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>点击左侧工具栏的画笔图标</h4>
|
||||
<p>点击左侧工具栏的画笔图标</p>
|
||||
<img class="step-screenshot" src="screenshots/初始状态-ed5fe35074b8f3a288f8493bd3cdf4b45091ebb2.png" alt="点击左侧工具栏的画笔图标">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>在画布上按住鼠标左键并拖动</h4>
|
||||
<p>在画布上按住鼠标左键并拖动</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-1-选择画笔工具-8631038478d440f3e891d2426ef4774902c06450.png" alt="在画布上按住鼠标左键并拖动">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>松开鼠标完成绘制</h4>
|
||||
<p>松开鼠标完成绘制</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-2-绘制线条-dab6cc0bc18592e88db1a1f86e4e294c1a2dcc6c.png" alt="松开鼠标完成绘制">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成</p>
|
||||
<p>生成时间: 2026/1/24 12:59:05</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,228 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>创建形状 - Aitu 用户手册</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #F39C12;
|
||||
--secondary-color: #5A4FCF;
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--border-color: #e0e0e0;
|
||||
--code-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header .version {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.sidebar-nav .category {
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 270px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-section h2 {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-section p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.steps {
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-screenshot {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="index.html"><strong>首页</strong></a></li>
|
||||
<li class="category">绘图功能</li>
|
||||
<li><a href="使用画笔工具绘制.html">使用画笔工具</a></li>
|
||||
<li><a href="创建形状.html">创建形状</a></li>
|
||||
<li><a href="添加文本.html">添加文本</a></li>
|
||||
<li class="category">AI 生成</li>
|
||||
<li><a href="使用-ai-生成图片.html">使用 AI 生成图片</a></li>
|
||||
<li><a href="使用灵感创意板.html">使用灵感创意板</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
<article class="page-section">
|
||||
<h2>创建形状</h2>
|
||||
<p>学习如何在画布上创建各种形状</p>
|
||||
|
||||
<div class="steps">
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>点击左侧工具栏的形状图标</h4>
|
||||
<p>点击左侧工具栏的形状图标</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-1-选择形状工具-ec381680b235070b0ea939f0ed84f2b53d7b8c49.png" alt="点击左侧工具栏的形状图标">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>在画布上点击要放置形状的位置</h4>
|
||||
<p>在画布上点击要放置形状的位置</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-2-创建形状-5445f0b842e82f308fb0327fce414199ad9691b2.png" alt="在画布上点击要放置形状的位置">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>形状将自动创建,可以拖动调整大小</h4>
|
||||
<p>形状将自动创建,可以拖动调整大小</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成</p>
|
||||
<p>生成时间: 2026/1/24 12:59:05</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,237 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>添加文本 - Aitu 用户手册</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #F39C12;
|
||||
--secondary-color: #5A4FCF;
|
||||
--text-color: #333;
|
||||
--bg-color: #fff;
|
||||
--border-color: #e0e0e0;
|
||||
--code-bg: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header .version {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.sidebar-nav .category {
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 270px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-section h2 {
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-section p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.steps {
|
||||
counter-reset: step;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-screenshot {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="index.html"><strong>首页</strong></a></li>
|
||||
<li class="category">绘图功能</li>
|
||||
<li><a href="使用画笔工具绘制.html">使用画笔工具</a></li>
|
||||
<li><a href="创建形状.html">创建形状</a></li>
|
||||
<li><a href="添加文本.html">添加文本</a></li>
|
||||
<li class="category">AI 生成</li>
|
||||
<li><a href="使用-ai-生成图片.html">使用 AI 生成图片</a></li>
|
||||
<li><a href="使用灵感创意板.html">使用灵感创意板</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
<article class="page-section">
|
||||
<h2>添加文本</h2>
|
||||
<p>学习如何在画布上添加文本</p>
|
||||
|
||||
<div class="steps">
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>点击左侧工具栏的文本图标</h4>
|
||||
<p>点击左侧工具栏的文本图标</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-1-选择文本工具-12e5c2b674a1cde71cc43118e7725db772e425f5.png" alt="点击左侧工具栏的文本图标">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>在画布上点击要添加文本的位置</h4>
|
||||
<p>在画布上点击要添加文本的位置</p>
|
||||
<img class="step-screenshot" src="screenshots/步骤-2-输入文本-8163f84062744e0fa2af5a687879c940edc299f0.png" alt="在画布上点击要添加文本的位置">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>输入您想要的文字内容</h4>
|
||||
<p>输入您想要的文字内容</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">4</div>
|
||||
<div class="step-content">
|
||||
<h4>点击画布空白处完成编辑</h4>
|
||||
<p>点击画布空白处完成编辑</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Aitu 用户手册 v0.5.48 | 由 E2E 测试自动生成</p>
|
||||
<p>生成时间: 2026/1/24 12:59:05</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -57,8 +57,9 @@ export const DialogTaskList: React.FC<DialogTaskListProps> = ({
|
||||
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, string> = {
|
||||
[TaskStatus.PENDING]: '待处理',
|
||||
[TaskStatus.PENDING]: '处理中',
|
||||
[TaskStatus.PROCESSING]: '处理中',
|
||||
[TaskStatus.COMPLETED]: '已完成',
|
||||
[TaskStatus.FAILED]: '失败',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -112,8 +112,9 @@ export const PromptInput: React.FC<PromptInputProps> = ({
|
||||
}
|
||||
}, [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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<MCPResult> {
|
||||
* 支持批量创建任务(通过 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,
|
||||
},
|
||||
|
||||
@@ -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<MCPResult> {
|
||||
* 支持批量创建任务(通过 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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user