From fecb4fe1d174fd6927849191ad8b2ead79cef20b Mon Sep 17 00:00:00 2001 From: Agions <1051736049@qq.com> Date: Tue, 7 Jul 2026 15:45:41 +0800 Subject: [PATCH 01/17] =?UTF-8?q?refactor(pipeline):=20=E6=B6=88=E9=99=A4?= =?UTF-8?q?=20computeMetrics=20=E5=92=8C=20execute()=20=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8F=96=E6=A8=A1=E6=9D=BF=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BasePipelineStep 增强: - 新增 computeCountMetric(fieldName) 辅助方法,统一 6 个步骤的数组长度指标计算 - 新增 computeNumericMetric(fieldName) 辅助方法,统一数值字段指标计算 - 新增 computeQualityGate() 钩子,RenderStep/VideoEditingStep 不再需要覆盖 execute() - execute() 方法内联构建 StepOutput,消除对 createSuccessStepResult 的依赖 - QualityGateDecision 改为值导入(非 type),解决运行时报错 各步骤文件: - computeMetrics 从 5-7 行缩减为 1 行(调用 computeCountMetric) - RenderStep/VideoEditingStep 删除 30 行 execute() 覆盖代码,改用 computeQualityGate 钩子 消除 ~80 行重复代码 --- src/core/pipeline/base-pipeline-step.ts | 54 +++++++++++++++++----- src/core/pipeline/step-analysis.ts | 9 +--- src/core/pipeline/step-audio-synthesis.ts | 9 +--- src/core/pipeline/step-character.ts | 9 +--- src/core/pipeline/step-import.ts | 5 +-- src/core/pipeline/step-render.ts | 55 ++++++++--------------- src/core/pipeline/step-script.ts | 6 +-- src/core/pipeline/step-storyboard.ts | 5 +-- src/core/pipeline/step-video-editing.ts | 48 ++++++-------------- 9 files changed, 79 insertions(+), 121 deletions(-) diff --git a/src/core/pipeline/base-pipeline-step.ts b/src/core/pipeline/base-pipeline-step.ts index eec7638f..cd8a5214 100644 --- a/src/core/pipeline/base-pipeline-step.ts +++ b/src/core/pipeline/base-pipeline-step.ts @@ -8,21 +8,17 @@ import type { RetryPolicy, PipelineStepId, } from './pipeline.types'; -import { PipelineExecutionMode } from './pipeline.types'; -import { - createFailedStepResult, - createSuccessStepResult, - reportStepProgress, - DEFAULT_RETRY_POLICY, -} from './step-helpers'; +import { PipelineExecutionMode, StepStatus, QualityGateDecision } from './pipeline.types'; +import { createFailedStepResult, reportStepProgress, DEFAULT_RETRY_POLICY } from './step-helpers'; /** * Pipeline Step 基类 * * 子类只需实现 executeImpl() 和配置属性,无需重复: * - 统一构造器样板(id/name/stepId/mode/retryPolicy/onProgress/dependencies) - * - 统一 execute()(耗时统计 + try/catch + 标准化 StepOutput) + * - 统一 execute()(耗时统计 + try/catch + 标准化 StepOutput + qualityGate) * - 统一 reportProgress() + * - 统一 computeMetrics()(通过 computeCountMetric 辅助) */ export abstract class BasePipelineStep implements PipelineStep { readonly id: string; @@ -49,10 +45,19 @@ export abstract class BasePipelineStep implements PipelineStep { const startTime = Date.now(); try { const result = await this.executeImpl(input); - return createSuccessStepResult(this.stepId, startTime, result, { - durationMs: Date.now() - startTime, - ...this.computeMetrics(result), - }); + return { + stepId: this.stepId, + status: StepStatus.COMPLETED, + data: result, + metrics: { + durationMs: Date.now() - startTime, + ...this.computeMetrics(result), + }, + qualityGate: this.computeQualityGate(result) ?? QualityGateDecision.PASS, + startTime, + endTime: Date.now(), + retryCount: 0, + }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); logger.error(`[${this.name}] failed: ${msg}`); @@ -71,4 +76,29 @@ export abstract class BasePipelineStep implements PipelineStep { protected computeMetrics(_result: unknown): Record { return {}; } + + /** 子类可覆盖,根据结果计算质量门控决策(默认 PASS) */ + protected computeQualityGate(_result: unknown): QualityGateDecision | undefined { + return undefined; + } + + /** 辅助方法:从结果中提取数值字段作为 framesProcessed */ + protected computeNumericMetric(result: unknown, fieldName: string): Record { + if (result && typeof result === 'object' && fieldName in (result as Record)) { + const val = (result as Record)[fieldName]; + if (typeof val === 'number') { + return { framesProcessed: val }; + } + } + return {}; + } + + /** 辅助方法:从结果中提取数组字段的长度作为 framesProcessed */ + protected computeCountMetric(result: unknown, fieldName: string): Record { + if (result && typeof result === 'object' && fieldName in (result as Record)) { + const arr = (result as Record)[fieldName]; + return { framesProcessed: Array.isArray(arr) ? arr.length : 0 }; + } + return {}; + } } diff --git a/src/core/pipeline/step-analysis.ts b/src/core/pipeline/step-analysis.ts index d0b620ff..3f989a78 100644 --- a/src/core/pipeline/step-analysis.ts +++ b/src/core/pipeline/step-analysis.ts @@ -63,14 +63,7 @@ export class AnalysisStep extends BasePipelineStep { } protected computeMetrics(result: unknown): Record { - if ( - result && - typeof result === 'object' && - 'estimatedScenes' in (result as Record) - ) { - return { framesProcessed: (result as { estimatedScenes: number }).estimatedScenes }; - } - return {}; + return this.computeNumericMetric(result, 'estimatedScenes'); } private estimateCharacterCount(chapters: ImportOutput['chapters']): number { diff --git a/src/core/pipeline/step-audio-synthesis.ts b/src/core/pipeline/step-audio-synthesis.ts index 3ab789c9..f6bc4467 100644 --- a/src/core/pipeline/step-audio-synthesis.ts +++ b/src/core/pipeline/step-audio-synthesis.ts @@ -72,14 +72,7 @@ export class AudioSynthesisStep extends BasePipelineStep { } protected computeMetrics(result: unknown): Record { - if ( - result && - typeof result === 'object' && - 'dialogueAudio' in (result as Record) - ) { - return { framesProcessed: (result as { dialogueAudio: unknown[] }).dialogueAudio.length }; - } - return {}; + return this.computeCountMetric(result, 'dialogueAudio'); } } diff --git a/src/core/pipeline/step-character.ts b/src/core/pipeline/step-character.ts index 0a3c24b3..16e46304 100644 --- a/src/core/pipeline/step-character.ts +++ b/src/core/pipeline/step-character.ts @@ -96,14 +96,7 @@ export class CharacterStep extends BasePipelineStep { } protected computeMetrics(result: unknown): Record { - if ( - result && - typeof result === 'object' && - 'characters' in (result as Record) - ) { - return { framesProcessed: (result as { characters: unknown[] }).characters.length }; - } - return {}; + return this.computeCountMetric(result, 'characters'); } private extractCharacterNames(scenes: Array<{ description: string }>): string[] { diff --git a/src/core/pipeline/step-import.ts b/src/core/pipeline/step-import.ts index 67e43fc5..9b13e116 100644 --- a/src/core/pipeline/step-import.ts +++ b/src/core/pipeline/step-import.ts @@ -115,10 +115,7 @@ export class ImportStep extends BasePipelineStep { } protected computeMetrics(result: unknown): Record { - if (result && typeof result === 'object' && 'chapters' in (result as Record)) { - return { framesProcessed: (result as { chapters: unknown[] }).chapters.length }; - } - return {}; + return this.computeCountMetric(result, 'chapters'); } private detectContentType(content: string): 'novel' | 'script' | 'prompt' { diff --git a/src/core/pipeline/step-render.ts b/src/core/pipeline/step-render.ts index 972283d7..c24b47d9 100644 --- a/src/core/pipeline/step-render.ts +++ b/src/core/pipeline/step-render.ts @@ -2,15 +2,7 @@ import { imageGenerationService } from '@/core/services/ai/image/image-generatio import { logger } from '@/core/utils/logger'; import { BasePipelineStep } from './base-pipeline-step'; -import { - PipelineStepId, - PipelineStep, - StepInput, - StepOutput, - StepStatus, - QualityGateDecision, -} from './pipeline.types'; -import { createFailedStepResult } from './step-helpers'; +import { PipelineStepId, PipelineStep, StepInput, QualityGateDecision } from './pipeline.types'; import type { StoryboardOutput } from './step-storyboard'; export interface RenderOutput { @@ -47,35 +39,24 @@ export class RenderStep extends BasePipelineStep { this.batchSize = config?.parallelKeys?.length ? Math.min(config.parallelKeys.length, 4) : 4; } - async execute(input: StepInput): Promise { - const startTime = Date.now(); - try { - const data = (await this.executeImpl(input)) as { - renderedFrames: Array<{ frameId: string; imageUrl: string }>; - failedFrames: string[]; - totalFrames: number; - successRate: number; - }; - - return { - stepId: this.stepId, - status: StepStatus.COMPLETED, - data, - metrics: { - durationMs: Date.now() - startTime, - framesProcessed: data.totalFrames, - qualityScore: data.successRate, - }, - qualityGate: data.successRate >= 0.8 ? QualityGateDecision.PASS : QualityGateDecision.WARN, - startTime, - endTime: Date.now(), - retryCount: 0, - }; - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - logger.error(`[RenderStep] Render failed: ${msg}`); - return createFailedStepResult(this.stepId, startTime, msg); + protected computeMetrics(result: unknown): Record { + if (result && typeof result === 'object') { + const r = result as Record; + const totalFrames = typeof r.totalFrames === 'number' ? r.totalFrames : 0; + const successRate = typeof r.successRate === 'number' ? r.successRate : 0; + return { framesProcessed: totalFrames, qualityScore: successRate }; } + return {}; + } + + protected computeQualityGate(result: unknown): QualityGateDecision | undefined { + if (result && typeof result === 'object') { + const successRate = (result as Record).successRate; + if (typeof successRate === 'number') { + return successRate >= 0.8 ? QualityGateDecision.PASS : QualityGateDecision.WARN; + } + } + return undefined; } protected async executeImpl(input: StepInput): Promise { diff --git a/src/core/pipeline/step-script.ts b/src/core/pipeline/step-script.ts index b5257612..0f2c01be 100644 --- a/src/core/pipeline/step-script.ts +++ b/src/core/pipeline/step-script.ts @@ -100,11 +100,7 @@ export class ScriptStep extends BasePipelineStep { if (typeof result === 'string') { return { tokensUsed: result.length }; } - if (result && typeof result === 'object' && 'scenes' in (result as Record)) { - const r = result as { scenes: unknown[] }; - return { framesProcessed: r.scenes.length }; - } - return {}; + return this.computeCountMetric(result, 'scenes'); } private buildScriptPrompt( diff --git a/src/core/pipeline/step-storyboard.ts b/src/core/pipeline/step-storyboard.ts index e6c3ad1e..48a8d47e 100644 --- a/src/core/pipeline/step-storyboard.ts +++ b/src/core/pipeline/step-storyboard.ts @@ -91,10 +91,7 @@ export class StoryboardStep extends BasePipelineStep { } protected computeMetrics(result: unknown): Record { - if (result && typeof result === 'object' && 'frames' in (result as Record)) { - return { framesProcessed: (result as { frames: unknown[] }).frames.length }; - } - return {}; + return this.computeCountMetric(result, 'frames'); } private buildShotPrompt( diff --git a/src/core/pipeline/step-video-editing.ts b/src/core/pipeline/step-video-editing.ts index 37172bce..44c79e72 100644 --- a/src/core/pipeline/step-video-editing.ts +++ b/src/core/pipeline/step-video-editing.ts @@ -14,9 +14,8 @@ import { tauriService } from '@/infrastructure/tauri-bridge/commands'; import { delay, PROCESSING_DELAY_MS } from '@/shared/utils'; import { BasePipelineStep } from './base-pipeline-step'; -import { PipelineStepId, QualityGateDecision, StepStatus } from './pipeline.types'; -import type { PipelineStep, StepInput, StepOutput } from './pipeline.types'; -import { createFailedStepResult } from './step-helpers'; +import { PipelineStepId, QualityGateDecision } from './pipeline.types'; +import type { PipelineStep, StepInput } from './pipeline.types'; import type { VideoClip, SubtitleBlock, @@ -40,32 +39,18 @@ export class VideoEditingStep extends BasePipelineStep { }); } - async execute(input: StepInput): Promise { - const startTime = Date.now(); - try { - const data = await this.executeImpl(input); - - const clips = (data as { clips: VideoClip[] }).clips; - const successRate = clips.length > 0 ? 1 : 0; - - return { - stepId: this.stepId, - status: StepStatus.COMPLETED, - data, - metrics: { - durationMs: Date.now() - startTime, - framesProcessed: clips.length, - }, - qualityGate: successRate >= 0.8 ? QualityGateDecision.PASS : QualityGateDecision.WARN, - startTime, - endTime: Date.now(), - retryCount: 0, - }; - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - logger.error(`[VideoEditingStep] Video editing failed: ${msg}`); - return createFailedStepResult(this.stepId, startTime, msg); + protected computeMetrics(result: unknown): Record { + return this.computeCountMetric(result, 'clips'); + } + + protected computeQualityGate(result: unknown): QualityGateDecision | undefined { + if (result && typeof result === 'object') { + const clips = (result as Record).clips; + if (Array.isArray(clips)) { + return clips.length > 0 ? QualityGateDecision.PASS : QualityGateDecision.WARN; + } } + return undefined; } protected async executeImpl(input: StepInput): Promise { @@ -182,13 +167,6 @@ export class VideoEditingStep extends BasePipelineStep { } as VideoEditingOutput; } - protected computeMetrics(result: unknown): Record { - if (result && typeof result === 'object' && 'clips' in (result as Record)) { - return { framesProcessed: (result as { clips: VideoClip[] }).clips.length }; - } - return {}; - } - private async exportVideo(editor: VideoEditor, workflowId: string): Promise { const clips = editor.exportConfig().clips; const timestamp = Date.now(); From b16ff999d1239a9252f6c14025b4435ddf866127 Mon Sep 17 00:00:00 2001 From: Agions <1051736049@qq.com> Date: Wed, 8 Jul 2026 09:50:47 +0800 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E9=9D=9E?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=80=A7=E8=87=AA=E5=8A=A8=E9=A9=BE=E9=A9=B6?= =?UTF-8?q?=E5=BC=95=E6=93=8E=20(AutoPipelineEngine)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除整个 core/autonomous/ 模块及 features/auto-pipeline/ 页面, 原因: loadSteps() 始终返回空数组,自审循环和质量门禁均为空壳, 整个流水线从未执行任何实际步骤,约 2500 行死代码。 同步删除: - /auto-pipeline 路由 (app/index.tsx) - page-preload 中的 autoPipeline 入口 - 5 个关联测试套件 (autonomous engine, self-review-loop, quality-gat) Co-Authored-By: Claude Opus 4.6 <> --- .../auto-pipeline-engine.integration.test.ts | 232 --------- .../autonomous/auto-pipeline-engine.test.ts | 64 --- .../packages/autonomous/quality-gate.test.ts | 66 --- .../autonomous/self-review-loop.test.ts | 41 -- src/app/index.tsx | 11 - src/app/router/page-preload.ts | 1 - src/core/autonomous/auto-pipeline-engine.ts | 460 ------------------ .../evaluator/quality-gate-config.ts | 132 ----- .../evaluator/quality-gate-evaluators.ts | 166 ------- src/core/autonomous/evaluator/quality-gate.ts | 116 ----- .../autonomous/evaluator/self-review-loop.ts | 157 ------ .../evaluator/self-review-parsers.ts | 104 ---- .../evaluator/self-review-prompt-templates.ts | 98 ---- src/core/autonomous/index.ts | 33 -- src/core/autonomous/pipeline-checkpoint.ts | 109 ----- .../autonomous/pipeline-event-dispatcher.ts | 133 ----- src/core/autonomous/pipeline-executor.ts | 66 --- src/core/autonomous/pipeline-step-state.ts | 77 --- src/core/autonomous/pipeline-types.ts | 38 -- src/core/autonomous/types/autonomous.types.ts | 294 ----------- src/core/pipeline/base-pipeline-step.ts | 3 +- src/core/pipeline/step-video-editing.ts | 9 +- src/core/services/ai/base-ai-service.ts | 4 +- src/core/services/pipeline/pipeline-runner.ts | 3 +- .../pipeline/review-export.service.ts | 3 +- .../components/AIBriefingPanel.tsx | 163 ------- .../components/AutoPipelineWizard.tsx | 181 ------- .../components/AutonomousProgress.tsx | 155 ------ .../auto-pipeline/components/FinalPreview.tsx | 155 ------ .../auto-pipeline/hooks/useAutoPipeline.ts | 134 ----- .../hooks/useSelfReviewLoop.reducer.ts | 73 --- .../auto-pipeline/hooks/useSelfReviewLoop.ts | 81 --- src/features/auto-pipeline/index.ts | 13 - .../services/autoPipelineService.ts | 64 --- .../stores/autoPipelineStore.test.ts | 423 ---------------- .../auto-pipeline/stores/autoPipelineStore.ts | 194 -------- .../services/video-compositor.service.ts | 9 +- src/pages/auto-pipeline/AutoPipelinePage.tsx | 49 -- src/shared/utils/data.ts | 6 + src/shared/utils/general.ts | 3 +- src/shared/utils/index.ts | 3 + 41 files changed, 22 insertions(+), 4104 deletions(-) delete mode 100644 src/__tests__/core/autonomous/auto-pipeline-engine.integration.test.ts delete mode 100644 src/__tests__/core/autonomous/auto-pipeline-engine.test.ts delete mode 100644 src/__tests__/packages/autonomous/quality-gate.test.ts delete mode 100644 src/__tests__/packages/autonomous/self-review-loop.test.ts delete mode 100644 src/core/autonomous/auto-pipeline-engine.ts delete mode 100644 src/core/autonomous/evaluator/quality-gate-config.ts delete mode 100644 src/core/autonomous/evaluator/quality-gate-evaluators.ts delete mode 100644 src/core/autonomous/evaluator/quality-gate.ts delete mode 100644 src/core/autonomous/evaluator/self-review-loop.ts delete mode 100644 src/core/autonomous/evaluator/self-review-parsers.ts delete mode 100644 src/core/autonomous/evaluator/self-review-prompt-templates.ts delete mode 100644 src/core/autonomous/index.ts delete mode 100644 src/core/autonomous/pipeline-checkpoint.ts delete mode 100644 src/core/autonomous/pipeline-event-dispatcher.ts delete mode 100644 src/core/autonomous/pipeline-executor.ts delete mode 100644 src/core/autonomous/pipeline-step-state.ts delete mode 100644 src/core/autonomous/pipeline-types.ts delete mode 100644 src/core/autonomous/types/autonomous.types.ts delete mode 100644 src/features/auto-pipeline/components/AIBriefingPanel.tsx delete mode 100644 src/features/auto-pipeline/components/AutoPipelineWizard.tsx delete mode 100644 src/features/auto-pipeline/components/AutonomousProgress.tsx delete mode 100644 src/features/auto-pipeline/components/FinalPreview.tsx delete mode 100644 src/features/auto-pipeline/hooks/useAutoPipeline.ts delete mode 100644 src/features/auto-pipeline/hooks/useSelfReviewLoop.reducer.ts delete mode 100644 src/features/auto-pipeline/hooks/useSelfReviewLoop.ts delete mode 100644 src/features/auto-pipeline/index.ts delete mode 100644 src/features/auto-pipeline/services/autoPipelineService.ts delete mode 100644 src/features/auto-pipeline/stores/autoPipelineStore.test.ts delete mode 100644 src/features/auto-pipeline/stores/autoPipelineStore.ts delete mode 100644 src/pages/auto-pipeline/AutoPipelinePage.tsx diff --git a/src/__tests__/core/autonomous/auto-pipeline-engine.integration.test.ts b/src/__tests__/core/autonomous/auto-pipeline-engine.integration.test.ts deleted file mode 100644 index c668b87e..00000000 --- a/src/__tests__/core/autonomous/auto-pipeline-engine.integration.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * AutoPipelineEngine 集成测试(v3.2 性能优化阶段) - * - * 单元测试覆盖:构造、idle 状态、event handler、并发拒绝 → src/core/autonomous/auto-pipeline-engine.test.ts - * 集成测试(本文件)覆盖:端到端 run() 全流程 + 真实 executeStep 路径 - * - * 测试矩阵(5 类场景): - * 1. Happy path:3 个 step 顺序执行,dispatch 事件顺序正确,最终 result 含 stepDurations - * 2. 依赖图:A→B→C,B 的 input 包含 A 的 output - * 3. Step 抛错:execute 抛异常 → 整体失败 + dispatch fail - * 4. Self-Review 失败重试:quality gate 第一次 fail,第二次 pass(验证重试循环) - * 5. disabled step 跳过:enabled=false 的 step 不执行 - * - * 策略:mock 掉 SelfReviewLoop / QualityGate 让行为可控; - * 用工厂 options.steps 注入 fake step 链覆盖默认空数组。 - */ - -import { - AutoPipelineEngine, - createAutoPipelineEngine, -} from '@/core/autonomous/auto-pipeline-engine'; -import type { PipelineStep, StepInput } from '@/core/autonomous/pipeline-types'; -import type { StepOutput } from '@/core/autonomous/types/autonomous.types'; - -// ============================================================================ -// 辅助:构造可控制的 mock step -// ============================================================================ - -interface MockStepOptions { - stepId: string; - name?: string; - enabled?: boolean; - dependencies?: string[]; - output?: StepOutput; - /** 每次 execute 抛错(覆盖 output) */ - throwError?: string; - /** 每次 execute 返回不同 output(按调用次数) */ - outputSequence?: StepOutput[]; - /** execute 调用时记录到 spy */ - onCall?: (callIndex: number, input: StepInput) => void; -} - -function makeMockStep(opts: MockStepOptions): PipelineStep & { _calls: number } { - const step = { - id: opts.stepId, - name: opts.name ?? opts.stepId, - stepId: opts.stepId, - enabled: opts.enabled ?? true, - maxRetries: 3, - timeout: 5000, - dependencies: opts.dependencies, - _calls: 0, - async execute(input: StepInput): Promise { - const idx = this._calls++; - opts.onCall?.(idx, input); - if (opts.throwError) { - throw new Error(opts.throwError); - } - if (opts.outputSequence) { - return opts.outputSequence[idx] ?? opts.outputSequence[opts.outputSequence.length - 1]; - } - return opts.output ?? { ok: true }; - }, - }; - return step; -} - -/** 通过工厂 + 覆盖内部 steps 字段 + 截断 loadSteps() 注入 mock 步骤链 */ -function createEngineWithSteps(steps: PipelineStep[]): AutoPipelineEngine { - const engine = createAutoPipelineEngine({ steps }); - - // 绕过 loadSteps() 空数组:直接覆盖内部 steps 字段,并 stub loadSteps - // @ts-expect-error - 内部字段访问,集成测试需要构造非空步骤链 - engine.steps = steps; - // @ts-expect-error - stub 私有方法 - engine.loadSteps = async () => steps; - return engine; -} - -// ============================================================================ -// 集成测试 -// ============================================================================ - -describe('AutoPipelineEngine — integration (v3.2)', () => { - // 收集所有 dispatch 事件 - function captureEvents(engine: AutoPipelineEngine) { - const events: string[] = []; - engine.onEvents({ - onStepStart: (id) => events.push(`step_start:${id}`), - onStepComplete: (id) => events.push(`step_complete:${id}`), - onStepFail: (id, err) => events.push(`step_fail:${id}:${err}`), - onPipelineStart: () => events.push('pipeline_start'), - onPipelineComplete: () => events.push('pipeline_complete'), - onPipelineFail: (err) => events.push(`pipeline_fail:${err}`), - onPipelineCancel: () => events.push('pipeline_cancel'), - }); - return events; - } - - // ========================================================================== - // 场景 1:Happy path — 3 个 step 顺序执行 - // ========================================================================== - it('runs 3 steps in order and dispatches correct events', async () => { - const stepA = makeMockStep({ stepId: 'step_a', output: { a: 1 } }); - const stepB = makeMockStep({ stepId: 'step_b', output: { b: 2 } }); - const stepC = makeMockStep({ stepId: 'step_c', output: { c: 3 } }); - - const engine = createEngineWithSteps([stepA, stepB, stepC]); - const events = captureEvents(engine); - - const result = await engine.run({ - content: 'test', - mode: 'novel', - style: 'anime', - qualityLevel: 'balanced', - }); - - expect(result.success).toBe(true); - expect(events).toEqual([ - 'pipeline_start', - 'step_start:step_a', - 'step_complete:step_a', - 'step_start:step_b', - 'step_complete:step_b', - 'step_start:step_c', - 'step_complete:step_c', - 'pipeline_complete', - ]); - expect(stepA._calls).toBe(1); - expect(stepB._calls).toBe(1); - expect(stepC._calls).toBe(1); - }); - - // ========================================================================== - // 场景 2:依赖图合并 — 后续 step 的 input 包含前序 step 的 output - // ========================================================================== - it('merges dependency outputs into next step input', async () => { - const stepA = makeMockStep({ stepId: 'step_a', output: { aValue: 'A' } }); - const stepB = makeMockStep({ - stepId: 'step_b', - dependencies: ['step_a'], - output: { bValue: 'B' }, - onCall: (_idx, input) => { - // step_b 应该看到 step_a 的 output - expect(input.aValue).toBe('A'); - }, - }); - - const engine = createEngineWithSteps([stepA, stepB]); - captureEvents(engine); - - const result = await engine.run({ content: 'merge', mode: 'novel' }); - expect(result.success).toBe(true); - expect(stepB._calls).toBe(1); - }); - - // ========================================================================== - // 场景 3:Step 抛错 → 整体失败 + dispatch pipeline_fail - // ========================================================================== - it('aborts pipeline when a step throws', async () => { - const stepA = makeMockStep({ stepId: 'step_a', output: { a: 1 } }); - const stepB = makeMockStep({ stepId: 'step_b', throwError: 'LLM timeout' }); - const stepC = makeMockStep({ stepId: 'step_c', output: { c: 3 } }); // 永远不应执行 - - const engine = createEngineWithSteps([stepA, stepB, stepC]); - const events = captureEvents(engine); - - const result = await engine.run({ content: 'fail', mode: 'novel' }); - expect(result.success).toBe(false); - expect(result.error).toContain('LLM timeout'); - expect(stepC._calls).toBe(0); // 关键:C 不应执行 - // 真实事件是 step_fail:step_b:LLM timeout(带 error 详情) - expect(events.some((e) => e.startsWith('step_fail:step_b'))).toBe(true); - expect(events.some((e) => e.startsWith('pipeline_fail'))).toBe(true); - // pipeline_complete 不应出现 - expect(events).not.toContain('pipeline_complete'); - }); - - // ========================================================================== - // 场景 4:Self-Review 失败重试(outputSequence 模拟修复过程) - // ========================================================================== - it('retries step when output needs improvement (outputSequence)', async () => { - const stepA = makeMockStep({ - stepId: 'step_a', - // 第一次返回不完整,第二次完整 - outputSequence: [{ quality: 'low' }, { quality: 'high' }], - }); - - const engine = createEngineWithSteps([stepA]); - const events = captureEvents(engine); - - const result = await engine.run({ content: 'retry', mode: 'novel' }); - expect(result.success).toBe(true); - // 至少调用一次(具体次数由 quality gate 配置决定) - expect(stepA._calls).toBeGreaterThanOrEqual(1); - expect(events).toContain('pipeline_complete'); - }); - - // ========================================================================== - // 场景 5:disabled step 跳过 - // ========================================================================== - it('skips disabled steps', async () => { - const stepA = makeMockStep({ stepId: 'step_a', enabled: true, output: { a: 1 } }); - const stepB = makeMockStep({ stepId: 'step_b', enabled: false, output: { b: 2 } }); - const stepC = makeMockStep({ stepId: 'step_c', enabled: true, output: { c: 3 } }); - - const engine = createEngineWithSteps([stepA, stepB, stepC]); - const events = captureEvents(engine); - - const result = await engine.run({ content: 'skip', mode: 'novel' }); - expect(result.success).toBe(true); - expect(stepB._calls).toBe(0); // 关键:disabled 不执行 - // step_b 不应有 step_start/Complete - expect(events.some((e) => e.startsWith('step_start:step_b'))).toBe(false); - expect(events).toContain('step_complete:step_a'); - expect(events).toContain('step_complete:step_c'); - }); - - // ========================================================================== - // 场景 6:result 含 stepDurations(每个 step 至少 0ms) - // ========================================================================== - it('returns result with stepDurations on success', async () => { - const stepA = makeMockStep({ stepId: 'step_a', output: { ok: true } }); - const engine = createEngineWithSteps([stepA]); - captureEvents(engine); - - const result = await engine.run({ content: 'duration', mode: 'novel' }); - expect(result.success).toBe(true); - expect(result.stepDurations).toBeDefined(); - expect(result.stepDurations?.step_a).toBeGreaterThanOrEqual(0); - }); -}); diff --git a/src/__tests__/core/autonomous/auto-pipeline-engine.test.ts b/src/__tests__/core/autonomous/auto-pipeline-engine.test.ts deleted file mode 100644 index def005cf..00000000 --- a/src/__tests__/core/autonomous/auto-pipeline-engine.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * AutoPipelineEngine Smoke Test - * 验证:构造、idle 状态、event handler 注册、cancel API - */ -import { AutoPipelineEngine, createAutoPipelineEngine } from '@/core/autonomous/auto-pipeline-engine'; - -describe('AutoPipelineEngine', () => { - describe('factory', () => { - it('createAutoPipelineEngine returns a new instance', () => { - const engine = createAutoPipelineEngine(); - expect(engine).toBeInstanceOf(AutoPipelineEngine); - }); - - it('accepts maxReviewRetries / reviewModel options', () => { - const engine = createAutoPipelineEngine({ maxReviewRetries: 5, reviewModel: 'gpt-4' }); - expect(engine).toBeInstanceOf(AutoPipelineEngine); - }); - }); - - describe('initial state', () => { - it('starts in idle status (not running, not failed)', () => { - const engine = new AutoPipelineEngine(); - // No public getter for status; verify by attempting duplicate run - expect(engine).toBeInstanceOf(AutoPipelineEngine); - }); - }); - - describe('event handlers', () => { - it('onEvents registers a handler without throwing', () => { - const engine = new AutoPipelineEngine(); - const handler = { - onStepProgress: jest.fn(), - onPipelineComplete: jest.fn(), - onPipelineError: jest.fn(), - onSelfReview: jest.fn(), - }; - expect(() => engine.onEvents(handler)).not.toThrow(); - }); - - it('multiple handlers can be registered', () => { - const engine = new AutoPipelineEngine(); - engine.onEvents({ onStepProgress: jest.fn() }); - expect(() => engine.onEvents({ onPipelineComplete: jest.fn() })).not.toThrow(); - }); - }); - - describe('run() guards', () => { - it('rejects concurrent runs (second call rejects first still running)', async () => { - const engine = new AutoPipelineEngine(); - // First call will fail because no steps are loaded; we just test the guard message - const p1 = engine.run({ content: '', mode: 'novel', style: 'anime', qualityLevel: 'balanced' }); - const p2 = engine.run({ content: '', mode: 'novel', style: 'anime', qualityLevel: 'balanced' }); - const results = await Promise.allSettled([p1, p2]); - // One of them must throw "Pipeline already running" (the second) - const messages = results - .filter((r) => r.status === 'rejected') - .map((r) => (r as PromiseRejectedResult).reason?.message ?? ''); - // Either the second run is rejected, or both fail downstream - expect(results.every((r) => r.status === 'fulfilled' || r.status === 'rejected')).toBe(true); - // Loose: just ensure the engine didn't crash silently - expect(messages.length).toBeGreaterThanOrEqual(0); - }); - }); -}); diff --git a/src/__tests__/packages/autonomous/quality-gate.test.ts b/src/__tests__/packages/autonomous/quality-gate.test.ts deleted file mode 100644 index 4a9937d2..00000000 --- a/src/__tests__/packages/autonomous/quality-gate.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * QualityGate Smoke Test - * 验证:默认审核标准、构造器、eval API 行为 - */ -import { - QualityGate, - createQualityGate, - DEFAULT_REVIEW_CRITERIA, - DEFAULT_QUALITY_GATE_CONFIG, -} from '../../../core/autonomous/evaluator/quality-gate'; - -describe('QualityGate', () => { - describe('default review criteria', () => { - it('exposes DEFAULT_REVIEW_CRITERIA for known stepIds', () => { - expect(DEFAULT_REVIEW_CRITERIA.script).toBeDefined(); - expect(DEFAULT_REVIEW_CRITERIA.character).toBeDefined(); - expect(DEFAULT_REVIEW_CRITERIA.storyboard).toBeDefined(); - expect(DEFAULT_REVIEW_CRITERIA.render).toBeDefined(); - }); - - it('exposes DEFAULT_QUALITY_GATE_CONFIG map', () => { - expect(DEFAULT_QUALITY_GATE_CONFIG).toBeDefined(); - expect(typeof DEFAULT_QUALITY_GATE_CONFIG).toBe('object'); - }); - }); - - describe('factory', () => { - it('createQualityGate returns QualityGate instance', () => { - const gate = createQualityGate('script'); - expect(gate).toBeInstanceOf(QualityGate); - }); - }); - - describe('construction', () => { - it('accepts a QualityGateConfig', () => { - const gate = new QualityGate({ - enabled: true, - threshold: 70, - onFail: 'retry', - }); - expect(gate).toBeInstanceOf(QualityGate); - }); - - it('defaults enabled/threshold/onFail when omitted', () => { - const gate = new QualityGate({} as any); - expect(gate).toBeInstanceOf(QualityGate); - }); - }); - - describe('evaluate()', () => { - it('returns passed=true when gate is disabled (regardless of output)', () => { - const gate = new QualityGate({ enabled: false } as any); - const result = gate.evaluate('script', {} as any); - expect(result.passed).toBe(true); - expect(result.score).toBe(100); - }); - - it('returns a result object with passed + score fields', () => { - const gate = new QualityGate({ enabled: true, threshold: 70 } as any); - const result = gate.evaluate('script', { data: 'hello' } as any); - expect(result).toHaveProperty('passed'); - expect(result).toHaveProperty('score'); - expect(typeof result.score).toBe('number'); - }); - }); -}); diff --git a/src/__tests__/packages/autonomous/self-review-loop.test.ts b/src/__tests__/packages/autonomous/self-review-loop.test.ts deleted file mode 100644 index 31b291a0..00000000 --- a/src/__tests__/packages/autonomous/self-review-loop.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * SelfReviewLoop Smoke Test - * 验证:构造、循环 API、回调触发 - */ -import { - SelfReviewLoop, - createSelfReviewLoop, -} from '../../../core/autonomous/evaluator/self-review-loop'; - -describe('SelfReviewLoop', () => { - describe('factory', () => { - it('createSelfReviewLoop returns instance with default options', () => { - const loop = createSelfReviewLoop(); - expect(loop).toBeInstanceOf(SelfReviewLoop); - }); - - it('accepts maxRetries and model options', () => { - const loop = createSelfReviewLoop({ maxRetries: 5, model: 'gpt-4o' }); - expect(loop).toBeInstanceOf(SelfReviewLoop); - }); - }); - - describe('construction', () => { - it('direct construction works', () => { - const loop = new SelfReviewLoop(); - expect(loop).toBeInstanceOf(SelfReviewLoop); - }); - }); - - describe('review()', () => { - it('returns a result object with passed/feedback fields', async () => { - const loop = createSelfReviewLoop({ maxRetries: 0 }); - // Pass a minimal valid step output (review takes stepId + output, not gate result) - const result = await loop.review('script', { data: 'test' } as any); - expect(result).toBeDefined(); - expect(result).toHaveProperty('passed'); - expect(result).toHaveProperty('score'); - expect(typeof result.score).toBe('number'); - }); - }); -}); diff --git a/src/app/index.tsx b/src/app/index.tsx index 753cac0d..9c693885 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -23,7 +23,6 @@ const WorkflowPage = lazy(importers.workflow); const ProjectEditPage = lazy(importers.projectEdit); const ProjectDetailPage = lazy(importers.projectDetail); const SettingsPage = lazy(importers.settings); -const AutoPipelinePage = lazy(importers.autoPipeline); // 加载时的占位组件 const PageLoader = () => ( @@ -97,16 +96,6 @@ const router = createBrowserRouter([ ), }, - { - path: '/auto-pipeline', - element: ( - - }> - - - - ), - }, { path: '*', element: , diff --git a/src/app/router/page-preload.ts b/src/app/router/page-preload.ts index e784dff4..dbf4d882 100644 --- a/src/app/router/page-preload.ts +++ b/src/app/router/page-preload.ts @@ -6,7 +6,6 @@ const pageImporters = { projectEdit: () => import('@/pages/project-edit/ProjectEditPage'), projectDetail: () => import('@/pages/project-detail/ProjectDetailPage'), settings: () => import('@/pages/settings/SettingsPage'), - autoPipeline: () => import('@/pages/auto-pipeline/AutoPipelinePage'), } as const; const routeImporterMap: Array<{ prefix: string; importer: Importer }> = [ diff --git a/src/core/autonomous/auto-pipeline-engine.ts b/src/core/autonomous/auto-pipeline-engine.ts deleted file mode 100644 index 64e180c7..00000000 --- a/src/core/autonomous/auto-pipeline-engine.ts +++ /dev/null @@ -1,460 +0,0 @@ -/** - * AutoPipelineEngine — 全自动流水线引擎(Facade) - * - * 核心能力(保持不变): - * 1. 端到端自动化:输入原材料 → 输出成片,无需用户介入 - * 2. Self-Review Loop:每个 Step 配备自审,不合格自动修复重做 - * 3. Quality Gate:关键节点设置质量门禁,不达标不推进 - * 4. 断点续传:支持暂停/恢复,中断后可继续 - * 5. 降级策略:主模型不可用时自动切换备选 - * - * 重构思路:原 634 行单类混合了6类职责,现拆为5个子模块: - * - pipeline-types PipelineStep / StepInput / StepCheckpoint - * - pipeline-step-state applyStepStateTransition / collectStepDurations / computeProgressPercent - * - pipeline-checkpoint buildPipelineId / save/load + applyCheckpointToEngine + interval 常量 - * - pipeline-event-dispatcher 13 个 dispatchXxx 替代原 emit 大 switch - * - pipeline-executor executeStepWithTimeout / buildStepInput - * - * 主类保留对外 API:run / pause / resume / cancel / getStatus / getStepStates - * / onEvents / isRunning + 工厂函数 createAutoPipelineEngine。 - */ - -import { logger } from '@/core/utils/logger'; - -import { createQualityGate } from './evaluator/quality-gate'; -import { SelfReviewLoop, createSelfReviewLoop } from './evaluator/self-review-loop'; -import { - buildCheckpointSnapshot, - buildPipelineId, - CHECKPOINT_INTERVAL_MS, - loadCheckpointFromStorage, - saveCheckpointToStorage, -} from './pipeline-checkpoint'; -import { PipelineEventDispatcher } from './pipeline-event-dispatcher'; -import { buildStepInput, executeStepWithTimeout } from './pipeline-executor'; -import { - applyStepStateTransition, - collectStepDurations, - computeProgressPercent, -} from './pipeline-step-state'; -import type { PipelineStep } from './pipeline-types'; -import type { - AutoPipelineInput, - AutoPipelineResult, - PipelineEventHandler, - PipelineStatus, - StepOutput, -} from './types/autonomous.types'; - -/** - * 步骤默认实现集合(占位)。 - * 实际步骤从 core/pipeline/steps/ 动态导入。 - */ -const AUTONOMOUS_STEPS: PipelineStep[] = []; - -export class AutoPipelineEngine { - private status: PipelineStatus = 'idle'; - private currentStepId: string | null = null; - private stepStates: Map = new Map(); - private selfReview: SelfReviewLoop; - private handlers: PipelineEventHandler[]; - private events: PipelineEventDispatcher; - private abortController: AbortController | null = null; - private checkpointInterval: ReturnType | null = null; - private context: Map = new Map(); - private steps: PipelineStep[] = []; - - constructor( - options: { - selfReview?: SelfReviewLoop; - maxReviewRetries?: number; - reviewModel?: string; - } = {} - ) { - this.selfReview = - options.selfReview ?? - createSelfReviewLoop({ - maxRetries: options.maxReviewRetries ?? 3, - model: options.reviewModel ?? 'glm-5', - }); - this.handlers = []; - this.events = new PipelineEventDispatcher(this.handlers); - } - - // ============================================================================ - // 公共 API - // ============================================================================ - - async run(input: AutoPipelineInput): Promise { - if (this.status === 'running') { - throw new Error('Pipeline already running'); - } - - this.status = 'running'; - this.abortController = new AbortController(); - - this.steps = await this.loadSteps(); - - // 如果 context 中已有 __input__(resume 场景),保留它;否则设置新 input - if (!this.context.has('__input__')) { - this.context.set('__input__', input as unknown as StepOutput); - } - - const startTime = Date.now(); - this.events.dispatchPipelineStart(); - this.startCheckpointInterval(); - - try { - for (const step of this.steps) { - if (!step.enabled) { - applyStepStateTransition(this.stepStates, step.stepId, 'skipped'); - continue; - } - - // 跳过已完成的步骤(resume 场景) - const currentState = this.stepStates.get(step.stepId); - if (currentState?.status === 'completed') { - continue; - } - - if (this.abortController.signal.aborted) { - this.status = 'cancelled'; - this.events.dispatchPipelineCancel(); - return { success: false, error: 'Pipeline cancelled by user' }; - } - - this.currentStepId = step.stepId; - const stepResult = await this.executeStep(step); - - if (!stepResult.success) { - this.status = 'failed'; - this.events.dispatchPipelineFail(`Step ${step.name} failed: ${stepResult.error}`); - return { - success: false, - error: `Step ${step.name} failed: ${stepResult.error}`, - }; - } - } - - this.status = 'completed'; - const duration = Date.now() - startTime; - const result: AutoPipelineResult = { - success: true, - outputPath: this.context.get('step_export')?.outputPath as string | undefined, - duration: this.context.get('step_export')?.duration as number | undefined, - resolution: '1080p', - fileSize: this.context.get('step_export')?.fileSize as number | undefined, - stepDurations: collectStepDurations(this.stepStates), - sceneCount: (this.context.get('step_script') as unknown as { scenes?: { length: number } }) - ?.scenes?.length as number | undefined, - characterCount: ( - this.context.get('step_character') as unknown as { characters?: { length: number } } - )?.characters?.length as number | undefined, - renderedFrames: ( - this.context.get('step_render') as unknown as { renderedFrames?: { length: number } } - )?.renderedFrames?.length as number | undefined, - }; - - this.events.dispatchPipelineComplete({ ...result, duration }); - return result; - } catch (error) { - this.status = 'failed'; - const errorMessage = error instanceof Error ? error.message : String(error); - this.events.dispatchPipelineFail(errorMessage); - return { success: false, error: errorMessage }; - } finally { - this.stopCheckpointInterval(); - } - } - - pause(): boolean { - if (this.status !== 'running') return false; - this.status = 'paused'; - this.events.dispatchPipelinePause(); - return true; - } - - async resume(_input?: AutoPipelineInput): Promise { - if (this.status !== 'paused') { - throw new Error('Pipeline is not paused'); - } - - this.status = 'running'; - this.events.dispatchPipelineResume(); - - const pipelineId = buildPipelineId( - this.context.get('__input__') as unknown as AutoPipelineInput | undefined - ); - const checkpoint = loadCheckpointFromStorage(pipelineId); - if (checkpoint) { - this.restoreFromCheckpoint(checkpoint); - } - - // 直接从中断点继续,不再调用 run() 重新初始化 - if (_input) { - this.context.set('__input__', _input as unknown as StepOutput); - } - - this.abortController = new AbortController(); - const startTime = Date.now(); - this.startCheckpointInterval(); - - try { - for (const step of this.steps) { - if (!step.enabled) { - applyStepStateTransition(this.stepStates, step.stepId, 'skipped'); - continue; - } - - const currentState = this.stepStates.get(step.stepId); - if (currentState?.status === 'completed') { - continue; - } - - if (this.abortController.signal.aborted) { - this.status = 'cancelled'; - this.events.dispatchPipelineCancel(); - return { success: false, error: 'Pipeline cancelled by user' }; - } - - this.currentStepId = step.stepId; - const stepResult = await this.executeStep(step); - - if (!stepResult.success) { - this.status = 'failed'; - this.events.dispatchPipelineFail(`Step ${step.name} failed: ${stepResult.error}`); - return { - success: false, - error: `Step ${step.name} failed: ${stepResult.error}`, - }; - } - } - - this.status = 'completed'; - const duration = Date.now() - startTime; - const result: AutoPipelineResult = { - success: true, - outputPath: this.context.get('step_export')?.outputPath as string | undefined, - duration: this.context.get('step_export')?.duration as number | undefined, - resolution: '1080p', - fileSize: this.context.get('step_export')?.fileSize as number | undefined, - stepDurations: collectStepDurations(this.stepStates), - sceneCount: (this.context.get('step_script') as unknown as { scenes?: { length: number } }) - ?.scenes?.length as number | undefined, - characterCount: ( - this.context.get('step_character') as unknown as { characters?: { length: number } } - )?.characters?.length as number | undefined, - renderedFrames: ( - this.context.get('step_render') as unknown as { renderedFrames?: { length: number } } - )?.renderedFrames?.length as number | undefined, - }; - - this.events.dispatchPipelineComplete({ ...result, duration }); - return result; - } catch (error) { - this.status = 'failed'; - const errorMessage = error instanceof Error ? error.message : String(error); - this.events.dispatchPipelineFail(errorMessage); - return { success: false, error: errorMessage }; - } finally { - this.stopCheckpointInterval(); - } - } - - cancel(): void { - if (this.abortController) { - this.abortController.abort(); - } - this.status = 'cancelled'; - this.events.dispatchPipelineCancel(); - } - - getStatus(): { status: PipelineStatus; currentStepId: string | null; progress: number } { - return { - status: this.status, - currentStepId: this.currentStepId, - progress: computeProgressPercent(this.stepStates, this.steps.length), - }; - } - - getStepStates(): Map { - return new Map(this.stepStates); - } - - onEvents(handler: PipelineEventHandler): void { - this.handlers.push(handler); - } - - isRunning(): boolean { - return this.status === 'running'; - } - - // ============================================================================ - // 私有方法 - // ============================================================================ - - /** - * 执行单个步骤(带自审循环 + 质量门禁)。 - * 行为与原 executeStep 逐字一致。 - */ - private async executeStep(step: PipelineStep): Promise<{ success: boolean; error?: string }> { - applyStepStateTransition(this.stepStates, step.stepId, 'running'); - logger.info(`[AutoPipeline] Starting step: ${step.name} (${step.stepId})`); - this.events.dispatchStepStart(step.stepId); - - const qualityGate = createQualityGate(step.stepId); - - const input = buildStepInput(step, this.context); - - let output: StepOutput; - try { - output = await executeStepWithTimeout(step, input); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - applyStepStateTransition(this.stepStates, step.stepId, 'failed', { error: errorMessage }); - this.events.dispatchStepFail(step.stepId, errorMessage); - return { success: false, error: errorMessage }; - } - - let reviewCount = 0; - const maxRetries = qualityGate.isSelfReviewEnabled() ? qualityGate.getMaxReviewRetries() : 0; - - while (true) { - const gateResult = qualityGate.evaluate(step.stepId, output); - - if (gateResult.passed) { - logger.info( - `[AutoPipeline] Step ${step.name} passed quality gate (score: ${gateResult.score})` - ); - this.events.dispatchQualityGate(step.stepId, { - passed: true, - score: gateResult.score, - details: gateResult.details, - }); - break; - } - - reviewCount += 1; - logger.warn( - `[AutoPipeline] Step ${step.name} failed quality gate (attempt ${reviewCount}), score: ${gateResult.score}` - ); - - applyStepStateTransition(this.stepStates, step.stepId, 'reviewing', { reviewCount }); - this.events.dispatchStepReviewStart(step.stepId, reviewCount); - - if (reviewCount > maxRetries) { - logger.error( - `[AutoPipeline] Step ${step.name} exceeded max review retries (${maxRetries})` - ); - this.events.dispatchStepFail( - step.stepId, - `Quality gate failed after ${maxRetries} retries` - ); - applyStepStateTransition(this.stepStates, step.stepId, 'failed', { - error: `Quality gate failed: ${gateResult.details}`, - }); - return { success: false, error: `Quality gate failed: ${gateResult.details}` }; - } - - const reviewResult = await this.selfReview.review(step.stepId, output); - - if (reviewResult.passed) { - logger.info( - `[AutoPipeline] Review passed, but repairing dimensions: ${reviewResult.dimensions - .filter((d) => !d.passed) - .map((d) => d.dimension) - .join(', ')}` - ); - } - - const repairedOutput = await this.selfReview.repair(step.stepId, output, reviewResult); - - if (repairedOutput !== output) { - output = repairedOutput; - logger.info(`[AutoPipeline] Step ${step.name} output repaired (attempt ${reviewCount})`); - } - - this.events.dispatchStepReviewComplete(step.stepId, reviewResult); - } - - this.context.set(step.stepId, output); - applyStepStateTransition(this.stepStates, step.stepId, 'completed', { output }); - this.events.dispatchStepComplete(step.stepId, output); - this.saveCheckpoint(step.stepId, output, reviewCount); - - return { success: true }; - } - - /** 保存检查点(pipelineId + 状态快照 → localStorage) */ - private saveCheckpoint(stepId: string, output: StepOutput, reviewCount: number): void { - const checkpoint = buildCheckpointSnapshot({ - pipelineId: buildPipelineId( - this.context.get('__input__') as unknown as AutoPipelineInput | undefined - ), - status: this.status, - currentStepId: stepId, - stepStates: this.stepStates, - context: this.context as unknown as Map, - now: Date.now(), - }); - saveCheckpointToStorage(checkpoint); - void reviewCount; // 保持调用签名不变(原行为也不使用) - } - - /** 从 checkpoint 恢复 stepStates + context */ - private restoreFromCheckpoint( - checkpoint: import('./types/autonomous.types').PipelineCheckpoint - ): void { - for (const [stepId, stepState] of Object.entries(checkpoint.steps)) { - const state = stepState as unknown as import('./types/autonomous.types').StepState; - this.stepStates.set(stepId, state); - if (stepState.data) { - this.context.set(stepId, stepState.data); - } - } - } - - /** 启动自动检查点定时器(30 秒) */ - private startCheckpointInterval(): void { - this.checkpointInterval = setInterval(() => { - if (this.status === 'running' && this.currentStepId) { - const currentOutput = this.context.get(this.currentStepId) as StepOutput | undefined; - if (currentOutput) { - this.saveCheckpoint(this.currentStepId, currentOutput, 0); - } - } - }, CHECKPOINT_INTERVAL_MS); - } - - /** 停止检查点定时器 */ - private stopCheckpointInterval(): void { - if (this.checkpointInterval) { - clearInterval(this.checkpointInterval); - this.checkpointInterval = null; - } - } - - /** 动态加载步骤链(占位实现) */ - private async loadSteps(): Promise { - // 实际从 core/pipeline/steps/ 动态导入 - void AUTONOMOUS_STEPS; - return []; - } -} - -/** - * 工厂函数:创建 AutoPipelineEngine,可选注入步骤链。 - */ -export function createAutoPipelineEngine(options?: { - maxReviewRetries?: number; - reviewModel?: string; - steps?: PipelineStep[]; -}): AutoPipelineEngine { - const engine = new AutoPipelineEngine(options); - - if (options?.steps) { - (engine as unknown as { steps: PipelineStep[] }).steps = options.steps; - } - - return engine; -} diff --git a/src/core/autonomous/evaluator/quality-gate-config.ts b/src/core/autonomous/evaluator/quality-gate-config.ts deleted file mode 100644 index 3d26e61d..00000000 --- a/src/core/autonomous/evaluator/quality-gate-config.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Quality Gate 配置集中 - * ===================== - * 两个大字典:DEFAULT_REVIEW_CRITERIA + DEFAULT_QUALITY_GATE_CONFIG - * 从 quality-gate.ts 中抽出,便于维护和更新阈值。 - * 单一职责:配置字典,无逻辑。 - */ -import type { QualityGateConfig, ReviewCriteria } from '../types/autonomous.types'; - -// ============================================================================ -// 各步骤的默认审核标准 -// ============================================================================ - -export const DEFAULT_REVIEW_CRITERIA: Record = { - [/* IMPORT */ '']: { - dimensions: ['completeness'], - minScorePerDimension: 60, - minTotalScore: 60, - minPassedDimensions: 1, - }, - script: { - dimensions: ['completeness', 'consistency', 'visual_quality', 'duration_match', 'punch_point'], - minScorePerDimension: 60, - minTotalScore: 70, - minPassedDimensions: 4, - }, - character: { - dimensions: ['completeness', 'consistency', 'visual_quality'], - minScorePerDimension: 65, - minTotalScore: 70, - minPassedDimensions: 3, - }, - storyboard: { - dimensions: ['completeness', 'consistency', 'visual_quality', 'duration_match'], - minScorePerDimension: 60, - minTotalScore: 65, - minPassedDimensions: 3, - }, - render: { - dimensions: ['completeness', 'visual_quality'], - minScorePerDimension: 50, - minTotalScore: 60, - minPassedDimensions: 1, - }, - audio: { - dimensions: ['completeness', 'duration_match'], - minScorePerDimension: 60, - minTotalScore: 60, - minPassedDimensions: 1, - }, -}; - -// ============================================================================ -// 各步骤的质量门禁默认配置 -// ============================================================================ - -export const DEFAULT_QUALITY_GATE_CONFIG: Record = { - import: { - enabled: true, - threshold: 60, - onFail: 'stop', - reviewConfig: { enabled: false, maxRetries: 0 }, - }, - analysis: { - enabled: true, - threshold: 60, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 2 }, - }, - script: { - enabled: true, - threshold: 70, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 3 }, - }, - character: { - enabled: true, - threshold: 70, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 3 }, - }, - scene: { - enabled: true, - threshold: 65, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 2 }, - }, - storyboard: { - enabled: true, - threshold: 65, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 3 }, - }, - render: { - enabled: true, - threshold: 60, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 2 }, - }, - 'video-edit': { - enabled: true, - threshold: 65, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 2 }, - }, - audio: { - enabled: true, - threshold: 60, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 2 }, - }, - subtitle: { - enabled: true, - threshold: 60, - onFail: 'skip', - reviewConfig: { enabled: false, maxRetries: 0 }, - }, - export: { - enabled: true, - threshold: 70, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 3 }, - }, -}; - -/** 工厂 fallback 配置(未知 stepId 时) */ -export const FALLBACK_GATE_CONFIG: QualityGateConfig = { - enabled: true, - threshold: 65, - onFail: 'retry', - reviewConfig: { enabled: true, maxRetries: 2 }, -}; diff --git a/src/core/autonomous/evaluator/quality-gate-evaluators.ts b/src/core/autonomous/evaluator/quality-gate-evaluators.ts deleted file mode 100644 index 1b1ebb85..00000000 --- a/src/core/autonomous/evaluator/quality-gate-evaluators.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * 基础检查 + 默认评分逻辑 - * ======================= - * 5 个 step_import/script/character/render/export 的基础检查 switch - * + 4 个 step_analysis/script/storyboard/audio 的默认评分 switch。 - * - * 抽成纯函数,QualityGate 类内部再调用。 - * 单一职责:评估逻辑,不含配置。 - */ -import type { StepOutput, QualityGateResult } from '../types/autonomous.types'; - -// ============================================================================ -// 基础检查 -// ============================================================================ - -/** - * 执行基础检查:空输出 / 步骤特定字段。 - * 返回 { passed, reason?, score }。 - */ -export function performBasicChecks( - stepId: string, - output: StepOutput -): { - passed: boolean; - reason?: string; - score: number; -} { - // 空输出检查 - if (!output || Object.keys(output).length === 0) { - return { passed: false, reason: 'Empty output', score: 0 }; - } - - // 步骤特定检查 - switch (stepId) { - case 'step_import': { - const chapters = output.chapters as Array | undefined; - if (!chapters || chapters.length === 0) { - return { passed: false, reason: 'No chapters found in import', score: 0 }; - } - const wordCount = (output.metadata as Record)?.wordCount as - | number - | undefined; - if (!wordCount || wordCount < 100) { - return { passed: false, reason: 'Word count too low (<100)', score: 30 }; - } - return { passed: true, score: 80 }; - } - - case 'step_script': { - const scenes = output.scenes as Array | undefined; - if (!scenes || scenes.length < 3) { - return { passed: false, reason: 'Insufficient scenes (<3)', score: 20 }; - } - const totalDuration = output.totalDuration as number | undefined; - if (!totalDuration || totalDuration < 60) { - return { passed: false, reason: 'Total duration too short (<60s)', score: 40 }; - } - return { passed: true, score: 75 }; - } - - case 'step_character': { - const characters = output.characters as Array | undefined; - if (!characters || characters.length === 0) { - return { passed: false, reason: 'No characters found', score: 0 }; - } - return { passed: true, score: 80 }; - } - - case 'step_render': { - const renderedFrames = output.renderedFrames as Array | undefined; - const totalFrames = output.totalFrames as number | undefined; - if (!renderedFrames || !totalFrames) { - return { passed: false, reason: 'Render output missing', score: 0 }; - } - const successRate = renderedFrames.length / totalFrames; - if (successRate < 0.5) { - return { - passed: false, - reason: `Success rate too low (${(successRate * 100).toFixed(0)}%)`, - score: 30, - }; - } - return { passed: true, score: Math.round(successRate * 100) }; - } - - case 'step_export': { - const outputPath = output.outputPath as string | undefined; - if (!outputPath) { - return { passed: false, reason: 'No output file path', score: 0 }; - } - return { passed: true, score: 100 }; - } - - default: - return { passed: true, score: 70 }; - } -} - -// ============================================================================ -// 默认质量评分 -// ============================================================================ - -/** - * 计算默认质量分数(无需 LLM,纯规则)。 - * 4 个步骤:analysis / script / storyboard / audio。 - * 默认步骤返回 70。 - */ -export function calculateDefaultScore(stepId: string, output: StepOutput): number { - switch (stepId) { - case 'step_analysis': { - const characters = output.characters as Array | undefined; - const scenes = output.scenes as Array | undefined; - let score = 50; - if (characters && characters.length > 0) score += 20; - if (scenes && scenes.length > 0) score += 20; - return score; - } - - case 'step_script': { - const scenes = output.scenes as Array | undefined; - let score = 50; - if (scenes && scenes.length >= 5) score += 25; - else if (scenes && scenes.length >= 3) score += 15; - const totalDuration = output.totalDuration as number | undefined; - if (totalDuration && totalDuration >= 300 && totalDuration <= 1800) score += 15; - return score; - } - - case 'step_storyboard': { - const shots = output.shots as Array | undefined; - let score = 50; - if (shots && shots.length >= 4) score += 30; - else if (shots && shots.length >= 2) score += 15; - return score; - } - - case 'step_audio': { - const duration = output.duration as number | undefined; - const targetDuration = output.targetDuration as number | undefined; - let score = 60; - if (duration && targetDuration) { - const deviation = Math.abs(duration - targetDuration) / targetDuration; - if (deviation < 0.05) score += 30; - else if (deviation < 0.1) score += 20; - else if (deviation < 0.2) score += 10; - else score -= 20; - } - return Math.max(0, Math.min(100, score)); - } - - default: - return 70; - } -} - -/** - * 使用审核标准进行评分(占位:实际由 LLM 完成)。 - * 返回标记需要 LLM-based review。 - */ -export function evaluateWithCriteriaPlaceholder(): QualityGateResult { - return { - passed: true, // 占位,实际由 SelfReviewLoop 决定 - details: 'Requires LLM-based review', - score: 100, - }; -} diff --git a/src/core/autonomous/evaluator/quality-gate.ts b/src/core/autonomous/evaluator/quality-gate.ts deleted file mode 100644 index 5a122f4e..00000000 --- a/src/core/autonomous/evaluator/quality-gate.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * QualityGate — 质量门禁(Facade) - * ================================ - * 每个 Pipeline Step 完成后,必须通过 QualityGate 判定质量。 - * 不合格的输出会触发 Self-Review Loop 进行自动修复。 - * - * 拆分思路(2 个 sibling 模块): - * - quality-gate-config.ts 两个配置字典 (REVIEW_CRITERIA + GATE_CONFIG) - * - quality-gate-evaluators.ts 基础检查 + 默认评分 + 占位 LLM 评分 - * - quality-gate.ts (facade) QualityGate 类 (薄壳) + createQualityGate 工厂 - * - * 调用方不需要修改。 - */ - -import type { - QualityGateResult, - QualityGateConfig, - StepOutput, - ReviewCriteria, -} from '../types/autonomous.types'; - -import { DEFAULT_QUALITY_GATE_CONFIG, FALLBACK_GATE_CONFIG } from './quality-gate-config'; -import { - performBasicChecks, - calculateDefaultScore, - evaluateWithCriteriaPlaceholder, -} from './quality-gate-evaluators'; - -// Re-export 配置字典(供外部使用) -export { DEFAULT_REVIEW_CRITERIA, DEFAULT_QUALITY_GATE_CONFIG } from './quality-gate-config'; - -// ============================================================================ -// QualityGate 类 -// ============================================================================ - -export class QualityGate { - private config: QualityGateConfig; - - constructor(config: QualityGateConfig) { - this.config = { - ...config, - enabled: config.enabled ?? true, - threshold: config.threshold ?? 70, - onFail: config.onFail ?? 'retry', - }; - } - - /** - * 评估 Step 输出质量。 - * - * 1. 未启用 → 直接通过 - * 2. 基础检查失败 → 直接不通过 - * 3. 有自定义 criteria → 占位 LLM 评分 - * 4. 默认规则 → 纯规则打分 + 阈值判定 - */ - evaluate(stepId: string, output: StepOutput, criteria?: ReviewCriteria): QualityGateResult { - if (!this.config.enabled) { - return { - passed: true, - details: 'Quality gate disabled', - score: 100, - }; - } - - // 基础检查 - const basicCheck = performBasicChecks(stepId, output); - if (!basicCheck.passed) { - return { - passed: false, - details: basicCheck.reason ?? 'Basic check failed', - score: basicCheck.score, - }; - } - - // 如果有自定义 criteria,进行评分 - if (criteria) { - return evaluateWithCriteriaPlaceholder(); - } - - // 使用默认规则 - const score = calculateDefaultScore(stepId, output); - const passed = score >= this.config.threshold; - - return { - passed, - details: passed - ? `Quality score ${score} meets threshold ${this.config.threshold}` - : `Quality score ${score} below threshold ${this.config.threshold}`, - score, - }; - } - - /** 获取 onFail 处理策略 */ - getOnFailStrategy(): 'retry' | 'skip' | 'stop' { - return this.config.onFail; - } - - /** 是否启用自审 */ - isSelfReviewEnabled(): boolean { - return this.config.reviewConfig?.enabled ?? false; - } - - /** 获取最大自审次数 */ - getMaxReviewRetries(): number { - return this.config.reviewConfig?.maxRetries ?? 2; - } -} - -// ============================================================================ -// 工厂函数 -// ============================================================================ - -export function createQualityGate(stepId: string): QualityGate { - const config = DEFAULT_QUALITY_GATE_CONFIG[stepId] ?? FALLBACK_GATE_CONFIG; - return new QualityGate(config); -} diff --git a/src/core/autonomous/evaluator/self-review-loop.ts b/src/core/autonomous/evaluator/self-review-loop.ts deleted file mode 100644 index ec1c7a76..00000000 --- a/src/core/autonomous/evaluator/self-review-loop.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * SelfReviewLoop — AI 自审循环(Facade) - * ====================================== - * 核心创新:当 Step 输出未通过 QualityGate 时, - * 调用 LLM 自我分析失败原因,重新生成修复后的输出。 - * 最多循环 maxRetries 次(默认 3 次)。 - * - * 拆分思路(2 个 sibling 模块): - * - self-review-prompt-templates.ts 2 个 prompt 模板 + STEP_NAMES + 温度/token 常量 - * - self-review-parsers.ts extractJson / parseReviewResult / parseJsonOutput - * - self-review-loop.ts (facade) SelfReviewLoop 类 (薄壳) + createSelfReviewLoop 工厂 - */ - -import { aiService } from '../../../../src/core/services/ai/text/ai.service'; -import { logger } from '../../../../src/core/utils/logger'; -import type { ReviewResult, StepOutput } from '../types/autonomous.types'; - -import { parseReviewResult, parseJsonOutput } from './self-review-parsers'; -import { - REVIEW_PROMPT_TEMPLATE, - REPAIR_PROMPT_TEMPLATE, - STEP_NAMES, - REVIEW_TEMPERATURE, - REVIEW_MAX_TOKENS, - REPAIR_TEMPERATURE, - REPAIR_MAX_TOKENS, -} from './self-review-prompt-templates'; - -export class SelfReviewLoop { - private maxRetries: number; - private model: string; - private reviewCount: Map = new Map(); - - constructor(options: { maxRetries?: number; model?: string } = {}) { - this.maxRetries = options.maxRetries ?? 3; - this.model = options.model ?? 'glm-5'; - } - - /** 审核 Step 输出 */ - async review(stepId: string, output: StepOutput): Promise { - const stepName = STEP_NAMES[stepId] ?? stepId; - - const prompt = REVIEW_PROMPT_TEMPLATE.replace('{stepName}', stepName).replace( - '{originalOutput}', - JSON.stringify(output, null, 2) - ); - - try { - const response = await aiService.generate(prompt, { - model: this.model, - provider: 'openai', - max_tokens: REVIEW_MAX_TOKENS, - temperature: REVIEW_TEMPERATURE, - }); - - return parseReviewResult(response); - } catch (error) { - // 审核失败时,默认通过(不阻塞流程) - logger.error(`[SelfReviewLoop] Review failed for ${stepId}:`, error); - return { - passed: true, - score: 70, - dimensions: [], - reasons: [], - suggestions: [], - }; - } - } - - /** 判定是否应该重试 */ - shouldRetry(stepId: string, result: ReviewResult): boolean { - const currentCount = this.reviewCount.get(stepId) ?? 0; - if (!result.passed && currentCount < this.maxRetries) { - return true; - } - return false; - } - - /** 增加重试计数 */ - incrementRetry(stepId: string): number { - const current = this.reviewCount.get(stepId) ?? 0; - const next = current + 1; - this.reviewCount.set(stepId, next); - return next; - } - - /** 重置重试计数 */ - reset(stepId: string): void { - this.reviewCount.delete(stepId); - } - - /** 获取当前重试次数 */ - getRetryCount(stepId: string): number { - return this.reviewCount.get(stepId) ?? 0; - } - - /** 修复 Step 输出 */ - async repair( - stepId: string, - originalOutput: StepOutput, - reviewResult: ReviewResult - ): Promise { - const stepName = STEP_NAMES[stepId] ?? stepId; - - const reasons = - reviewResult.reasons.length > 0 ? reviewResult.reasons.join('\n') : '综合评分未达标'; - - const suggestions = - reviewResult.suggestions.length > 0 - ? reviewResult.suggestions.join('\n') - : '请根据审核反馈优化输出质量'; - - const reviewResultText = ` -评分:${reviewResult.score}/100 -${reviewResult.dimensions - .map((d) => `${d.dimension}: ${d.score}分 ${d.passed ? '✓' : '✗'} - ${d.detail}`) - .join('\n')} -修复建议:${suggestions} -`.trim(); - - const prompt = REPAIR_PROMPT_TEMPLATE.replace(/{stepName}/g, stepName) - .replace('{originalOutput}', JSON.stringify(originalOutput, null, 2)) - .replace('{reviewResult}', reviewResultText) - .replace('{fallbackReasons}', reasons); - - try { - const response = await aiService.generate(prompt, { - model: this.model, - provider: 'openai', - max_tokens: REPAIR_MAX_TOKENS, - temperature: REPAIR_TEMPERATURE, - }); - - const repaired = parseJsonOutput(response); - return repaired ?? originalOutput; // 解析失败时返回原输出 - } catch (error) { - logger.error(`[SelfReviewLoop] Repair failed for ${stepId}:`, error); - return originalOutput; - } - } -} - -// ============================================================================ -// 工厂函数 -// ============================================================================ - -let sharedInstance: SelfReviewLoop | null = null; - -export function createSelfReviewLoop(options?: { - maxRetries?: number; - model?: string; -}): SelfReviewLoop { - if (!sharedInstance) { - sharedInstance = new SelfReviewLoop(options); - } - return sharedInstance; -} diff --git a/src/core/autonomous/evaluator/self-review-parsers.ts b/src/core/autonomous/evaluator/self-review-parsers.ts deleted file mode 100644 index 8125cc3f..00000000 --- a/src/core/autonomous/evaluator/self-review-parsers.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Self-Review Loop JSON 解析器 - * ============================ - * 从 LLM 回复中提取结构化 JSON 的 3 个方法。 - * - * extractJson:3 种策略(直接解析 → code block → 首尾大括号) - * parseReviewResult:提取 5 维度评分 - * parseJsonOutput:提取修复后的 JSON(as StepOutput) - * - * 单一职责:纯解析,无 AI 调用。 - */ -import type { ReviewResult, ReviewDimension, StepOutput } from '../types/autonomous.types'; - -/** - * 从文本中提取 JSON 对象。 - * 3 种策略(按优先级): - * 1. 直接 JSON.parse - * 2. ```json ... ``` code block - * 3. 首个 { ... 最后一个 } 之间的内容 - * - * 全部失败返回 null。 - */ -export function extractJson(text: string): Record | null { - // 策略 1:直接解析 - try { - return JSON.parse(text) as Record; - } catch { - // 继续 - } - - // 策略 2:从 ```json 块中提取 - const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/); - if (codeBlockMatch) { - try { - return JSON.parse(codeBlockMatch[1].trim()) as Record; - } catch { - // 继续 - } - } - - // 策略 3:首个 { ... 最后一个 } 之间的内容 - const firstBrace = text.indexOf('{'); - const lastBrace = text.lastIndexOf('}'); - if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { - try { - return JSON.parse(text.slice(firstBrace, lastBrace + 1)) as Record; - } catch { - // 放弃 - } - } - - return null; -} - -/** 解析失败时的默认 ReviewResult(通过,不阻塞流程) */ -const DEFAULT_REVIEW_RESULT: ReviewResult = { - passed: true, - score: 70, - dimensions: [], - reasons: [], - suggestions: [], -}; - -/** 分数值夹具:0-100 范围限制 */ -function clampScore(value: unknown): number { - return Math.max(0, Math.min(100, Number(value) || 0)); -} - -/** - * 解析审核结果(5 维度评分)。 - * 解析失败时返回 DEFAULT_REVIEW_RESULT(通过,不阻塞)。 - */ -export function parseReviewResult(response: string): ReviewResult { - try { - const json = extractJson(response); - if (!json) throw new Error('No JSON found'); - - return { - passed: Boolean(json.passed), - score: clampScore(json.score), - dimensions: Array.isArray(json.dimensions) - ? json.dimensions.map((d: Record) => ({ - dimension: (d.dimension as ReviewDimension) ?? 'completeness', - score: clampScore(d.score), - passed: Boolean(d.passed), - detail: String(d.detail ?? ''), - })) - : [], - reasons: Array.isArray(json.reasons) ? json.reasons.map(String) : [], - suggestions: Array.isArray(json.suggestions) ? json.suggestions.map(String) : [], - }; - } catch { - return DEFAULT_REVIEW_RESULT; - } -} - -/** - * 解析修复后的 JSON 输出(as StepOutput)。 - * 返回 null 表示解析失败,调用方回退到原输出。 - */ -export function parseJsonOutput(text: string): StepOutput | null { - const json = extractJson(text); - return json as StepOutput | null; -} diff --git a/src/core/autonomous/evaluator/self-review-prompt-templates.ts b/src/core/autonomous/evaluator/self-review-prompt-templates.ts deleted file mode 100644 index e08c2286..00000000 --- a/src/core/autonomous/evaluator/self-review-prompt-templates.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Self-Review Loop Prompt 模板 - * =========================== - * 两个大 prompt 模板 + 11 步 stepName 映射。 - * 便于单独调优 prompt 文案,不用改 SelfReviewLoop 类逻辑。 - */ - -/** 审核 prompt 模板(5 维度评分) */ -export const REVIEW_PROMPT_TEMPLATE = `你是专业的 AI 内容质量审核员。请审核以下 AI 生成的 {stepName} 输出。 - -## 审核维度 - -1. **完整性 (completeness)**:输出是否包含所有必要字段/元素? -2. **一致性 (consistency)**:人物描写、场景描述前后是否矛盾? -3. **画面感 (visual_quality)**:描述是否具备足够的视觉细节供 AI 生图? -4. **时长匹配 (duration_match)**:对话/场景时长是否与内容体量匹配? -5. **情绪爆点 (punch_point)**:是否包含情绪爆点、转折、高潮? - -## 原输出 - -{originalOutput} - -## 审核要求 - -请严格按照上述 5 个维度评分(0-100分),并给出: -1. 每个维度的评分和是否通过(>=60分通过) -2. 不合格的具体原因(列出所有未通过项) -3. 修复建议 - -## 输出格式 - -请严格按以下 JSON 格式输出,不要包含任何其他内容: - -{ - "passed": true/false, - "score": 0-100, - "dimensions": [ - {"dimension": "completeness", "score": 0-100, "passed": true/false, "detail": "说明"}, - {"dimension": "consistency", "score": 0-100, "passed": true/false, "detail": "说明"}, - {"dimension": "visual_quality", "score": 0-100, "passed": true/false, "detail": "说明"}, - {"dimension": "duration_match", "score": 0-100, "passed": true/false, "detail": "说明"}, - {"dimension": "punch_point", "score": 0-100, "passed": true/false, "detail": "说明"} - ], - "reasons": ["不合格原因1", "不合格原因2"], - "suggestions": ["修复建议1", "修复建议2"] -}`; - -/** 修复 prompt 模板(基于审核反馈重新生成) */ -export const REPAIR_PROMPT_TEMPLATE = `你是专业的 {stepName} 内容生成专家。以下是你之前生成的 {stepName} 输出和审核反馈。 - -## 原输出 - -{originalOutput} - -## 审核反馈 - -{reviewResult} - -## 不合格原因 - -{fallbackReasons} - -## 修复要求 - -请根据以上反馈,重新生成符合以下要求的 {stepName} 输出: -1. 修复所有不合格项 -2. 保持与上下文的连贯性 -3. 输出格式保持不变 -4. 只输出修复后的内容,不要包含任何解释 - -## 直接输出修复后的 JSON 内容:`; - -/** 步骤 ID → 中文名称映射 */ -export const STEP_NAMES: Record = { - 'step-import': '导入解析', - 'step-analysis': 'AI 分析', - 'step-script': '剧本生成', - 'step-character': '角色设计', - 'step-scene': '场景规划', - 'step-storyboard': '分镜生成', - 'step-render': '批量渲染', - 'step-video-edit': '视频剪辑', - 'step-audio': '配音合成', - 'step-subtitle': '字幕嵌入', - 'step-export': '成片导出', -}; - -/** 审核温度(低温 = 稳定) */ -export const REVIEW_TEMPERATURE = 0.3; - -/** 修复温度(较高温度 = 产生变化) */ -export const REPAIR_TEMPERATURE = 0.7; - -/** 审核最大 token 数 */ -export const REVIEW_MAX_TOKENS = 4096; - -/** 修复最大 token 数 */ -export const REPAIR_MAX_TOKENS = 8192; diff --git a/src/core/autonomous/index.ts b/src/core/autonomous/index.ts deleted file mode 100644 index 06b2ec56..00000000 --- a/src/core/autonomous/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * frame-fab Autonomous Mode — Core Module - * - * 全自动 AI 漫剧制作系统核心模块 - * - * @example - * ```typescript - * import { createAutoPipelineEngine } from './autonomous'; - * - * const engine = createAutoPipelineEngine({ maxReviewRetries: 3 }); - * - * engine.onEvents({ - * onStepProgress: (stepId, progress) => { - * console.log(`[${stepId}] Progress: ${progress}%`); - * }, - * onPipelineComplete: (result) => { - * console.log('Done! Output:', result.outputPath); - * }, - * }); - * - * const result = await engine.run({ - * content: '从前有座山,山里有座庙...', - * mode: 'novel', - * style: 'anime', - * qualityLevel: 'balanced', - * }); - * ``` - */ - -export * from './types/autonomous.types'; -export * from './evaluator/quality-gate'; -export * from './evaluator/self-review-loop'; -export * from './auto-pipeline-engine'; diff --git a/src/core/autonomous/pipeline-checkpoint.ts b/src/core/autonomous/pipeline-checkpoint.ts deleted file mode 100644 index 2f38d553..00000000 --- a/src/core/autonomous/pipeline-checkpoint.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Pipeline 检查点管理 - * - * 把原 AutoPipelineEngine 中关于"检查点持久化"的几块逻辑集中: - * - 构造 pipelineId - * - 保存到 localStorage(容错 + 写 warn 日志) - * - 从 localStorage 读取(容错 + 返回 null) - * - 把检查点恢复回 stepStates + context - * - 30 秒定时保存 interval(启动/停止) - * - * 单一职责:检查点生命周期与持久化。 - */ - -import { logger } from '@/core/utils/logger'; - -import type { - AutoPipelineInput, - PipelineCheckpoint, - PipelineStatus, - StepState, -} from './types/autonomous.types'; - -/** 检查点 localStorage key 前缀 */ -const CHECKPOINT_KEY_PREFIX = 'autopipeline_checkpoint_'; - -/** 自动检查点保存间隔(毫秒),与原实现一致 */ -export const CHECKPOINT_INTERVAL_MS = 30000; - -/** - * 构造 pipeline 唯一 ID。 - * 行为与原 getPipelineId 逐字一致:优先使用输入的 title,否则 fallback 到 `pipeline_`。 - */ -export function buildPipelineId(input: AutoPipelineInput | undefined | null): string { - return input?.title ?? `pipeline_${Date.now()}`; -} - -/** - * 把检查点写入 localStorage。失败时 warn 但不抛出。 - */ -export function saveCheckpointToStorage(checkpoint: PipelineCheckpoint): boolean { - try { - const key = `${CHECKPOINT_KEY_PREFIX}${checkpoint.pipelineId}`; - localStorage.setItem(key, JSON.stringify(checkpoint)); - return true; - } catch (error) { - logger.warn('[AutoPipeline] Failed to save checkpoint:', error); - return false; - } -} - -/** - * 从 localStorage 读取检查点。失败/缺失一律返回 null。 - */ -export function loadCheckpointFromStorage(pipelineId: string): PipelineCheckpoint | null { - try { - const key = `${CHECKPOINT_KEY_PREFIX}${pipelineId}`; - const stored = localStorage.getItem(key); - if (stored) { - return JSON.parse(stored) as PipelineCheckpoint; - } - } catch { - // 忽略错误(原行为) - } - return null; -} - -/** - * 构造一个 PipelineCheckpoint 快照(不负责持久化)。 - * 把 stepStates 转换为 Record。 - */ -export function buildCheckpointSnapshot(params: { - pipelineId: string; - status: PipelineStatus; - currentStepId: string; - stepStates: Map; - context: Map; - now: number; -}): PipelineCheckpoint { - const entries = Array.from(params.stepStates.entries()); - return { - pipelineId: params.pipelineId, - status: params.status, - currentStepId: params.currentStepId, - steps: Object.fromEntries(entries) as unknown as PipelineCheckpoint['steps'], - input: params.context.get('__input__') as unknown as AutoPipelineInput, - startedAt: params.now, - updatedAt: params.now, - }; -} - -/** - * 把 checkpoint.steps 还原到 stepStates Map,并把每个 step 的 data 还原到 context。 - * - * 行为与原 restoreFromCheckpoint 逐字一致。 - */ -export function applyCheckpointToEngine( - checkpoint: PipelineCheckpoint, - stepStates: Map, - context: Map -): void { - const stepsRecord = checkpoint.steps as unknown as Record; - for (const [stepId, stepState] of Object.entries(stepsRecord)) { - const state = stepState as unknown as StepState; - stepStates.set(stepId, state); - if (stepState.data) { - context.set(stepId, stepState.data); - } - } -} diff --git a/src/core/autonomous/pipeline-event-dispatcher.ts b/src/core/autonomous/pipeline-event-dispatcher.ts deleted file mode 100644 index 53bc7ee5..00000000 --- a/src/core/autonomous/pipeline-event-dispatcher.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Pipeline 事件分发器 - * - * 把原 AutoPipelineEngine.emit 中的 13-case switch 拆为独立的派发函数。 - * - 类型安全:每个事件有独立签名 - * - 调用方通过 dispatchXxxEvent(...) 调用,避免字符串魔数 - * - 兼容旧 emit(eventType, ...args) 调用方式(保留兼容层) - */ - -import type { - AutoPipelineResult, - PipelineEventHandler, - QualityGateResult, - ReviewResult, - StepOutput, -} from './types/autonomous.types'; - -// ─────────── 事件类型常量(保留兼容旧 emit 字符串协议) ─────────── - -const PIPELINE_EVENT = { - STEP_START: 'step_start', - STEP_PROGRESS: 'step_progress', - STEP_COMPLETE: 'step_complete', - STEP_FAIL: 'step_fail', - STEP_REVIEW_START: 'step_review_start', - STEP_REVIEW_COMPLETE: 'step_review_complete', - QUALITY_GATE: 'quality_gate', - PIPELINE_START: 'pipeline_start', - PIPELINE_COMPLETE: 'pipeline_complete', - PIPELINE_FAIL: 'pipeline_fail', - PIPELINE_PAUSE: 'pipeline_pause', - PIPELINE_RESUME: 'pipeline_resume', - PIPELINE_CANCEL: 'pipeline_cancel', -} as const; - -/** - * 类型化的事件派发器:把单 case 的实现拆成独立函数, - * 避免巨型 switch 难以阅读与扩展。 - */ -export class PipelineEventDispatcher { - constructor(private handlers: PipelineEventHandler[]) {} - - /** step_start(stepId) */ - dispatchStepStart(stepId: string): void { - for (const handler of this.handlers) { - handler.onStepStart?.(stepId); - } - } - - /** step_progress(stepId, progress, message) */ - dispatchStepProgress(stepId: string, progress: number, message: string): void { - for (const handler of this.handlers) { - handler.onStepProgress?.(stepId, progress, message); - } - } - - /** step_complete(stepId, output) */ - dispatchStepComplete(stepId: string, output: StepOutput): void { - for (const handler of this.handlers) { - handler.onStepComplete?.(stepId, output); - } - } - - /** step_fail(stepId, errorMessage) */ - dispatchStepFail(stepId: string, errorMessage: string): void { - for (const handler of this.handlers) { - handler.onStepFail?.(stepId, errorMessage); - } - } - - /** step_review_start(stepId, reviewCount) */ - dispatchStepReviewStart(stepId: string, reviewCount: number): void { - for (const handler of this.handlers) { - handler.onStepReviewStart?.(stepId, reviewCount); - } - } - - /** step_review_complete(stepId, reviewResult) */ - dispatchStepReviewComplete(stepId: string, reviewResult: ReviewResult): void { - for (const handler of this.handlers) { - handler.onStepReviewComplete?.(stepId, reviewResult); - } - } - - /** quality_gate(stepId, result) */ - dispatchQualityGate(stepId: string, result: QualityGateResult): void { - for (const handler of this.handlers) { - handler.onQualityGate?.(stepId, result); - } - } - - /** pipeline_start() */ - dispatchPipelineStart(): void { - for (const handler of this.handlers) { - handler.onPipelineStart?.(); - } - } - - /** pipeline_complete(result) */ - dispatchPipelineComplete(result: AutoPipelineResult): void { - for (const handler of this.handlers) { - handler.onPipelineComplete?.(result); - } - } - - /** pipeline_fail(errorMessage) */ - dispatchPipelineFail(errorMessage: string): void { - for (const handler of this.handlers) { - handler.onPipelineFail?.(errorMessage); - } - } - - /** pipeline_pause() */ - dispatchPipelinePause(): void { - for (const handler of this.handlers) { - handler.onPipelinePause?.(); - } - } - - /** pipeline_resume() */ - dispatchPipelineResume(): void { - for (const handler of this.handlers) { - handler.onPipelineResume?.(); - } - } - - /** pipeline_cancel() */ - dispatchPipelineCancel(): void { - for (const handler of this.handlers) { - handler.onPipelineCancel?.(); - } - } -} diff --git a/src/core/autonomous/pipeline-executor.ts b/src/core/autonomous/pipeline-executor.ts deleted file mode 100644 index ce090829..00000000 --- a/src/core/autonomous/pipeline-executor.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Pipeline 步骤执行辅助 - * - * 从 AutoPipelineEngine 中抽离: - * - executeWithTimeout:Promise.race 风格的超时控制 - * - buildStepInput:合并前序依赖输出 + 全局输入 - * - * 单一职责:步骤输入构造与执行超时控制。 - */ - -import type { PipelineStep, StepInput } from './pipeline-types'; -import type { StepOutput } from './types/autonomous.types'; - -/** - * 用 Promise 包装 step.execute,附带超时拒绝。 - * 行为与原 executeWithTimeout 逐字一致: - * - 超时错误消息:`Step ${step.name} timed out after ${step.timeout}ms` - * - 成功/失败都清理 timer - */ -export function executeStepWithTimeout(step: PipelineStep, input: StepInput): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error(`Step ${step.name} timed out after ${step.timeout}ms`)); - }, step.timeout); - - step - .execute(input) - .then((output) => { - clearTimeout(timer); - resolve(output); - }) - .catch((error) => { - clearTimeout(timer); - reject(error); - }); - }); -} - -/** - * 把"前序依赖步骤的输出" + "全局输入"合并为步骤输入。 - * - * 合并顺序: - * 1. 遍历 step.dependencies,从 context 取出每个 dep 的输出并浅合并 - * 2. 再把全局输入(context['__input__'])浅合并到 input(可覆盖 dep 输出) - * - * 行为与原 buildStepInput 逐字一致。 - */ -export function buildStepInput(step: PipelineStep, context: Map): StepInput { - const input: StepInput = {}; - - if (step.dependencies) { - for (const depId of step.dependencies) { - const depOutput = context.get(depId); - if (depOutput && typeof depOutput === 'object') { - Object.assign(input, depOutput); - } - } - } - - const globalInput = context.get('__input__'); - if (globalInput && typeof globalInput === 'object') { - Object.assign(input, globalInput); - } - - return input; -} diff --git a/src/core/autonomous/pipeline-step-state.ts b/src/core/autonomous/pipeline-step-state.ts deleted file mode 100644 index 7497eabb..00000000 --- a/src/core/autonomous/pipeline-step-state.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Pipeline 步骤状态管理 - * - * 把 AutoPipelineEngine 中的 - * - updateStepState - * - collectStepDurations - * - createInitialStepState - * 三个纯函数 / Map 更新操作抽离。 - * - * 单一职责:管理 StepState Map 的状态转换与统计。 - */ - -import type { StepState } from './types/autonomous.types'; - -/** 创建步骤的初始 pending 状态 */ -export function createInitialStepState(stepId: string): StepState { - return { - stepId, - name: stepId, - status: 'pending', - progress: 0, - reviewCount: 0, - }; -} - -/** - * 更新某个步骤的状态,自动维护 startedAt/completedAt 时间戳。 - * 行为与原 AutoPipelineEngine.updateStepState 逐字一致: - * - 进入 running 状态时设置 startedAt - * - 进入 completed/failed 时设置 completedAt - */ -export function applyStepStateTransition( - stepStates: Map, - stepId: string, - status: StepState['status'], - extra: Partial> = {} -): void { - const existing = stepStates.get(stepId) ?? createInitialStepState(stepId); - - const updated: StepState = { - ...existing, - status, - ...extra, - startedAt: existing.startedAt ?? (status === 'running' ? Date.now() : undefined), - completedAt: status === 'completed' || status === 'failed' ? Date.now() : undefined, - }; - - stepStates.set(stepId, updated); -} - -/** - * 收集所有有 start/end 时间的步骤的耗时。 - * 返回值:Record - */ -export function collectStepDurations(stepStates: Map): Record { - const durations: Record = {}; - for (const [stepId, state] of Array.from(stepStates.entries())) { - if (state.startedAt && state.completedAt) { - durations[stepId] = state.completedAt - state.startedAt; - } - } - return durations; -} - -/** - * 计算当前进度百分比(按已完成 + 跳过 步骤占总步骤的比例)。 - */ -export function computeProgressPercent( - stepStates: Map, - totalSteps: number -): number { - if (totalSteps <= 0) return 0; - const completed = Array.from(stepStates.values()).filter( - (s) => s.status === 'completed' || s.status === 'skipped' - ).length; - return Math.round((completed / totalSteps) * 100); -} diff --git a/src/core/autonomous/pipeline-types.ts b/src/core/autonomous/pipeline-types.ts deleted file mode 100644 index 4c59e4db..00000000 --- a/src/core/autonomous/pipeline-types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * AutoPipelineEngine 内部 Step 类型 - * - * 把原文件里的 PipelineStep / StepInput / StepCheckpoint 接口抽离。 - * 这些类型仅供 auto-pipeline-engine 内部及其子模块使用,不属于外部 API。 - */ - -import type { StepOutput } from './types/autonomous.types'; - -/** Pipeline 中单个步骤的配置与执行器 */ -export interface PipelineStep { - id: string; - name: string; - stepId: string; - enabled: boolean; - maxRetries: number; - timeout: number; - dependencies?: string[]; - execute(input: StepInput): Promise; - getCheckpoint?(): StepCheckpoint | null; - restore?(state: StepCheckpoint): void; - onProgress?: (event: { stepId: string; progress: number; message: string }) => void; -} - -/** 步骤输入(合并前序步骤的输出 + 全局输入) */ -export interface StepInput { - [key: string]: unknown; -} - -/** 单步骤检查点 */ -export interface StepCheckpoint { - stepId: string; - completed: boolean; - data: StepOutput; - reviewCount: number; - retryIndex: number; - timestamp: number; -} diff --git a/src/core/autonomous/types/autonomous.types.ts b/src/core/autonomous/types/autonomous.types.ts deleted file mode 100644 index 0d86d3fa..00000000 --- a/src/core/autonomous/types/autonomous.types.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * frame-fab Autonomous Mode — Core Types - * 全自动 AI 漫剧制作系统的核心类型定义 - */ - -// ============================================================================ -// 运行模式 -// ============================================================================ - -/** 当前 Autonomous Pipeline 状态 */ -export type AutonomousPipelineStatus = - | 'idle' - | 'running' - | 'paused' - | 'completed' - | 'failed' - | 'cancelled'; - -/** @deprecated 使用 AutonomousPipelineStatus */ -export type PipelineStatus = AutonomousPipelineStatus; - -/** 质量等级 */ -export type QualityLevel = 'fast' | 'balanced' | 'premium'; - -/** 漫剧风格 */ -export type MangaStyle = '2d' | '3d' | 'anime' | 'realistic'; - -// ============================================================================ -// 输入/输出 -// ============================================================================ - -/** 全自动 Pipeline 输入 */ -export interface AutoPipelineInput { - /** 原材料内容 */ - content: string; - /** 输入类型 */ - mode: 'novel' | 'script' | 'prompt'; - /** 项目标题(可选) */ - title?: string; - /** 风格选择 */ - style?: MangaStyle; - /** 质量等级 */ - qualityLevel?: QualityLevel; - /** 目标时长(分钟,可选) */ - targetDuration?: number; - /** 语言 */ - language?: 'zh' | 'en'; - /** 是否启用自审循环 */ - enableSelfReview?: boolean; - /** 最大自审循环次数 */ - maxReviewRetries?: number; -} - -/** 全自动 Pipeline 最终结果 */ -export interface AutoPipelineResult { - /** 是否成功 */ - success: boolean; - /** 最终输出文件路径 */ - outputPath?: string; - /** 生成的视频时长(秒) */ - duration?: number; - /** 分辨率 */ - resolution?: string; - /** 文件大小 */ - fileSize?: number; - /** 各步骤耗时 */ - stepDurations?: Record; - /** 错误信息 */ - error?: string; - /** 生成的场景数 */ - sceneCount?: number; - /** 生成的角色数 */ - characterCount?: number; - /** 渲染帧数 */ - renderedFrames?: number; -} - -// ============================================================================ -// Step 状态 -// ============================================================================ - -/** 单步状态 */ -export interface StepState { - stepId: string; - name: string; - status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'reviewing'; - progress: number; // 0-100 - message?: string; - startedAt?: number; - completedAt?: number; - reviewCount: number; - error?: string; - output?: StepOutput; -} - -/** Step 输出(通用) */ -export interface StepOutput { - [key: string]: unknown; -} - -// ============================================================================ -// Self-Review Loop -// ============================================================================ - -/** 自审结果 */ -export interface ReviewResult { - /** 是否通过 */ - passed: boolean; - /** 评分(0-100) */ - score: number; - /** 审核维度评分 */ - dimensions: ReviewDimensionScore[]; - /** 不合格原因列表 */ - reasons: string[]; - /** 修复建议 */ - suggestions: string[]; -} - -/** 审核维度评分 */ -export interface ReviewDimensionScore { - dimension: ReviewDimension; - score: number; - passed: boolean; - detail: string; -} - -/** 审核维度枚举 */ -export type ReviewDimension = - | 'completeness' // 完整性 - | 'consistency' // 一致性 - | 'visual_quality' // 画面感 - | 'duration_match' // 时长匹配 - | 'punch_point'; // 情绪爆点 - -/** 审核标准配置 */ -export interface ReviewCriteria { - dimensions: ReviewDimension[]; - /** 每维度最低分 */ - minScorePerDimension: number; - /** 综合最低分 */ - minTotalScore: number; - /** 最低通过维度数 */ - minPassedDimensions: number; -} - -// ============================================================================ -// Quality Gate -// ============================================================================ - -/** 质量门禁判定结果 */ -export interface QualityGateResult { - /** 是否通过 */ - passed: boolean; - /** 判定详情 */ - details: string; - /** 质量评分 */ - score: number; - /** 是否触发降级 */ - degraded?: boolean; - /** 降级原因 */ - degradationReason?: string; -} - -/** 质量门禁配置 */ -export interface QualityGateConfig { - /** 是否启用 */ - enabled: boolean; - /** 评分阈值 */ - threshold: number; - /** 失败处理策略 */ - onFail: 'retry' | 'skip' | 'stop'; - /** 自审配置 */ - reviewConfig?: { - enabled: boolean; - maxRetries: number; - }; -} - -// ============================================================================ -// Pipeline 事件 -// ============================================================================ - -/** Pipeline 事件类型 */ -export type PipelineEventType = - | 'step_start' - | 'step_progress' - | 'step_complete' - | 'step_fail' - | 'step_review_start' - | 'step_review_complete' - | 'quality_gate_pass' - | 'quality_gate_fail' - | 'pipeline_start' - | 'pipeline_complete' - | 'pipeline_fail' - | 'pipeline_pause' - | 'pipeline_resume' - | 'pipeline_cancel'; - -/** Pipeline 事件 */ -export interface PipelineEvent { - type: PipelineEventType; - timestamp: number; - stepId?: string; - progress?: number; - message?: string; - data?: unknown; - error?: string; -} - -/** Pipeline 事件处理器 */ -export interface PipelineEventHandler { - onStepStart?: (stepId: string) => void; - onStepProgress?: (stepId: string, progress: number, message?: string) => void; - onStepComplete?: (stepId: string, output: StepOutput) => void; - onStepFail?: (stepId: string, error: string) => void; - onStepReviewStart?: (stepId: string, attempt: number) => void; - onStepReviewComplete?: (stepId: string, result: ReviewResult) => void; - onQualityGate?: (stepId: string, result: QualityGateResult) => void; - onPipelineStart?: () => void; - onPipelineComplete?: (result: AutoPipelineResult) => void; - onPipelineFail?: (error: string) => void; - onPipelinePause?: () => void; - onPipelineResume?: () => void; - onPipelineCancel?: () => void; -} - -// ============================================================================ -// 检查点 -// ============================================================================ - -/** Step 检查点 */ -export interface StepCheckpoint { - stepId: string; - completed: boolean; - data: StepOutput; - output?: StepOutput; - reviewCount: number; - retryIndex: number; - timestamp: number; -} - -/** Pipeline 检查点 */ -export interface PipelineCheckpoint { - pipelineId: string; - status: PipelineStatus; - currentStepId?: string; - steps: Record; - input: AutoPipelineInput; - startedAt: number; - updatedAt: number; -} - -// ============================================================================ -// 步骤定义 -// ============================================================================ - -/** 步骤 ID 枚举(kebab-case 与 core/pipeline/pipeline.types.ts 对齐) */ -export enum AutonomousPipelineStepId { - IMPORT = 'step-import', - ANALYSIS = 'step-analysis', - SCRIPT = 'step-script', - CHARACTER = 'step-character', - SCENE = 'step-scene', - STORYBOARD = 'step-storyboard', - RENDER = 'step-render', - VIDEO_EDIT = 'step-video-edit', - AUDIO = 'step-audio', - SUBTITLE = 'step-subtitle', - EXPORT = 'step-export', -} - -/** @deprecated 使用 AutonomousPipelineStepId */ -export const PipelineStepId = AutonomousPipelineStepId; - -/** 步骤配置 */ -export interface StepConfig { - id: string; - name: string; - stepId: AutonomousPipelineStepId; - enabled: boolean; - maxRetries: number; - timeout: number; // ms - /** 自审配置 */ - reviewConfig?: { - enabled: boolean; - criteria: ReviewCriteria; - maxRetries: number; - }; - /** 质量门禁配置 */ - qualityGate?: QualityGateConfig; - /** 依赖步骤 */ - dependencies?: AutonomousPipelineStepId[]; -} diff --git a/src/core/pipeline/base-pipeline-step.ts b/src/core/pipeline/base-pipeline-step.ts index cd8a5214..3e83cacd 100644 --- a/src/core/pipeline/base-pipeline-step.ts +++ b/src/core/pipeline/base-pipeline-step.ts @@ -1,4 +1,5 @@ import { logger } from '@/core/utils/logger'; +import { getErrorMessage } from '@/shared/utils'; import type { PipelineStep, @@ -59,7 +60,7 @@ export abstract class BasePipelineStep implements PipelineStep { retryCount: 0, }; } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = getErrorMessage(error); logger.error(`[${this.name}] failed: ${msg}`); return createFailedStepResult(this.stepId, startTime, msg); } diff --git a/src/core/pipeline/step-video-editing.ts b/src/core/pipeline/step-video-editing.ts index 44c79e72..53fb11a6 100644 --- a/src/core/pipeline/step-video-editing.ts +++ b/src/core/pipeline/step-video-editing.ts @@ -11,7 +11,7 @@ import { logger } from '@/core/utils/logger'; import { tauriService } from '@/infrastructure/tauri-bridge/commands'; -import { delay, PROCESSING_DELAY_MS } from '@/shared/utils'; +import { delay, PROCESSING_DELAY_MS, isTauri } from '@/shared/utils'; import { BasePipelineStep } from './base-pipeline-step'; import { PipelineStepId, QualityGateDecision } from './pipeline.types'; @@ -172,7 +172,7 @@ export class VideoEditingStep extends BasePipelineStep { const timestamp = Date.now(); const outputPath = `output/${workflowId}/final_${timestamp}.mp4`; - if (this.isTauriEnvironment()) { + if (isTauri()) { try { await tauriService.exportVideo({ inputPath: clips[0]?.path ?? '', @@ -218,11 +218,6 @@ export class VideoEditingStep extends BasePipelineStep { return outputPath; } - - private isTauriEnvironment(): boolean { - if (typeof window === 'undefined') return false; - return '__TAURI__' in window; - } } // Re-export types for external consumers diff --git a/src/core/services/ai/base-ai-service.ts b/src/core/services/ai/base-ai-service.ts index 1ddbcdfd..14e5c13e 100644 --- a/src/core/services/ai/base-ai-service.ts +++ b/src/core/services/ai/base-ai-service.ts @@ -26,7 +26,7 @@ */ import { logger } from '@/core/utils/logger'; -import { retryRequest } from '@/shared/utils'; +import { getErrorMessage, retryRequest } from '@/shared/utils'; // ========== Error Types ========== @@ -255,7 +255,7 @@ export abstract class BaseAIService { code = 'NETWORK_ERROR'; } - const message = error instanceof Error ? error.message : String(error); + const message = getErrorMessage(error); logger.error(`[${this.serviceName}] request failed`, { endpoint, diff --git a/src/core/services/pipeline/pipeline-runner.ts b/src/core/services/pipeline/pipeline-runner.ts index 16d0ad19..fb605bf3 100644 --- a/src/core/services/pipeline/pipeline-runner.ts +++ b/src/core/services/pipeline/pipeline-runner.ts @@ -16,6 +16,7 @@ */ import { logger } from '@/core/utils/logger'; +import { getErrorMessage } from '@/shared/utils'; import type { PipelineCallbacks, @@ -209,7 +210,7 @@ export class PipelineRunner { return stepResult; } catch (error) { stepResult.status = 'error'; - stepResult.error = error instanceof Error ? error.message : String(error); + stepResult.error = getErrorMessage(error); stepResult.endTime = Date.now(); stepResult.duration = stepResult.endTime - stepResult.startTime; diff --git a/src/core/services/pipeline/review-export.service.ts b/src/core/services/pipeline/review-export.service.ts index 618d0568..5d7764fe 100644 --- a/src/core/services/pipeline/review-export.service.ts +++ b/src/core/services/pipeline/review-export.service.ts @@ -6,6 +6,7 @@ import type { FrameComment, StoryboardVersion } from '@/core/services/domain/collaboration.service'; import type { CostRecord, CostStats } from '@/core/services/project/cost.service'; import type { EvaluationScores } from '@/core/services/project/evaluation.service'; +import { getErrorMessage } from '@/shared/utils'; const REVIEW_EXPORT_ACTIVITY_KEY = 'frame-fab_review_export_activities'; @@ -200,7 +201,7 @@ class ReviewExportService { source: options.source || 'unknown', status: 'failed', fileName: defaultFileName, - errorMessage: error instanceof Error ? error.message : String(error), + errorMessage: getErrorMessage(error), }); throw error; } diff --git a/src/features/auto-pipeline/components/AIBriefingPanel.tsx b/src/features/auto-pipeline/components/AIBriefingPanel.tsx deleted file mode 100644 index edb915f4..00000000 --- a/src/features/auto-pipeline/components/AIBriefingPanel.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/** - * AIBriefingPanel — AI 任务简报面板 - * - * 展示当前 AI 正在执行的任务: - * - 任务目标 - * - 为什么要这样做 - * - 正在调用哪个模型 - * - 预计耗时 - */ - -import { Bot, Brain, Zap, Clock } from 'lucide-react'; - -import { cn } from '@/shared/utils/class-names'; - -interface AIBriefingPanelProps { - stepId: string; - stepName: string; - model?: string; - estimatedTime?: number; // seconds - reason?: string; - className?: string; -} - -const STEP_BRIEFINGS: Record< - string, - { - goal: string; - reason: string; - model: string; - defaultTime: number; - } -> = { - 'step-import': { - goal: '解析原材料', - reason: '自动识别小说/剧本格式,智能切分章节', - model: '内置解析器', - defaultTime: 10, - }, - 'step-analysis': { - goal: '分析故事结构', - reason: '识别人物、场景、情节曲线和情绪爆点', - model: 'GLM-5 / M2.5', - defaultTime: 60, - }, - 'step-script': { - goal: '生成视频剧本', - reason: '将小说文本转化为结构化的视频分镜脚本', - model: 'GLM-5', - defaultTime: 120, - }, - 'step-character': { - goal: '设计角色', - reason: '生成角色设定卡,保证跨镜头一致性', - model: 'Seedream 5.0 + GLM-5', - defaultTime: 180, - }, - 'step-scene': { - goal: '规划场景', - reason: '规划全局场景布局、色彩基调和氛围', - model: 'GLM-5', - defaultTime: 60, - }, - 'step-storyboard': { - goal: '生成分镜', - reason: '生成每个镜头的参考图和动作描述', - model: 'Seedream 5.0', - defaultTime: 300, - }, - 'step-render': { - goal: '批量渲染', - reason: 'AI 批量生成所有关键帧图像', - model: 'Seedream 5.0 / Kling 1.6', - defaultTime: 600, - }, - 'step-video-edit': { - goal: '剪辑合成', - reason: '将帧序列合成为连续视频,添加转场', - model: 'FFmpeg WASM', - defaultTime: 120, - }, - 'step-audio': { - goal: '配音合成', - reason: '文字转语音,生成角色对话和旁白', - model: 'Edge TTS / CosyVoice 2.0', - defaultTime: 180, - }, - 'step-subtitle': { - goal: '字幕嵌入', - reason: '生成时间轴字幕并嵌入视频', - model: '内置字幕引擎', - defaultTime: 60, - }, - 'step-export': { - goal: '导出成片', - reason: '最终编码输出 MP4/WebM 文件', - model: 'FFmpeg WASM', - defaultTime: 120, - }, -}; - -export function AIBriefingPanel({ - stepId, - stepName, - model, - estimatedTime, - reason, - className, -}: AIBriefingPanelProps) { - const briefing = STEP_BRIEFINGS[stepId] ?? { - goal: '处理中...', - reason: reason ?? 'AI 正在工作中', - model: model ?? '—', - defaultTime: 60, - }; - - const time = estimatedTime ?? briefing.defaultTime; - const timeLabel = time < 60 ? `${time}秒` : `${Math.round(time / 60)}分钟`; - - return ( -
- {/* 头部 */} -
- - {stepName} -
- - {/* 目标 */} -
-
- - 任务目标 -
-

{briefing.goal}

-
- - {/* 原因 */} -
-
- - 为什么要这样做 -
-

{briefing.reason}

-
- - {/* 底部信息 */} -
-
- 调用模型: - {model ?? briefing.model} -
-
- - 预计 {timeLabel} -
-
-
- ); -} diff --git a/src/features/auto-pipeline/components/AutoPipelineWizard.tsx b/src/features/auto-pipeline/components/AutoPipelineWizard.tsx deleted file mode 100644 index 358ce6e3..00000000 --- a/src/features/auto-pipeline/components/AutoPipelineWizard.tsx +++ /dev/null @@ -1,181 +0,0 @@ -/** - * AutoPipelineWizard — 一步式启动向导 - * - * 用户只需要: - * 1. 粘贴或上传小说/剧本 - * 2. 选择风格和质量等级 - * 3. 点击「开始制作」 - * - * 之后全部交给 AI 自主完成! - */ - -import { useState } from 'react'; - -import type { MangaStyle, QualityLevel } from '@/core/autonomous/types/autonomous.types'; -import { Button } from '@/shared/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/shared/components/ui/select'; -import { Textarea } from '@/shared/components/ui/textarea'; - -import { useAutoPipeline } from '../hooks/useAutoPipeline'; - -export function AutoPipelineWizard() { - const [content, setContent] = useState(''); - const [style, setStyle] = useState('anime'); - const [quality, setQuality] = useState('balanced'); - const [title, setTitle] = useState(''); - - const { start, isRunning, progress, currentStep, error } = useAutoPipeline(); - - const handleStart = () => { - if (!content.trim()) return; - - start({ - content: content.trim(), - mode: 'novel', - title: title.trim() || undefined, - style, - qualityLevel: quality, - enableSelfReview: true, - maxReviewRetries: 3, - }); - }; - - return ( -
- {/* 标题区 */} -
-

AI 全自动漫剧制作

-

- 粘贴你的小说或剧本,AI 自动完成从剧本解析到成片导出的全部工作 -

-
- - {/* 输入区 */} - - - 第一步:提交你的故事 - - - {/* 项目名称 */} -
- - setTitle(e.target.value)} - className="w-full px-3 py-2 border rounded-md" - disabled={isRunning} - /> -
- - {/* 内容输入 */} -
- -