import { Injectable } from '@angular/core'; import { CreatorDashboardAction, CreatorDashboardSummary } from '../models/creator-dashboard.model'; import { BatchJobService } from './batch-job.service'; import { ContentAssetService } from './content-asset.service'; import { GenerationTaskService } from './generation-task.service'; import { IndexedDbJsonStorage } from './indexed-db-json-storage'; import { TaskRecoveryService } from './task-recovery.service'; import { TopicPoolService } from './topic-pool.service'; @Injectable({ providedIn: 'root' }) export class CreatorDashboardService { constructor( private generationTasks: GenerationTaskService, private topicPool: TopicPoolService, private contentAssets: ContentAssetService, private batchJobs: BatchJobService, private taskRecovery: TaskRecoveryService, ) {} getSummary(): CreatorDashboardSummary { const tasks = this.generationTasks.tasks; const topics = this.topicPool.listTopics(); const assets = this.contentAssets.assets; const jobs = this.batchJobs.listJobs(); const runningTasks = tasks.filter((task) => task.status === 'running' || task.status === 'waiting_external').length; const recoverableTasks = tasks.filter((task) => task.recoverable && task.status !== 'completed' && task.status !== 'cancelled').length; const failedTasks = tasks.filter((task) => task.status === 'failed').length; const pendingTopics = topics.filter((topic) => topic.status === 'idea' || topic.status === 'script_ready').length; const readyAssets = assets.filter((asset) => asset.type === 'video' && asset.source?.isFinal && !asset.archived).length; const activeBatchJobs = jobs.filter((job) => job.status === 'queued' || job.status === 'running' || job.status === 'paused').length; const storageWarnings = IndexedDbJsonStorage.getAuditTrail() .filter((event) => event.status === 'fallback' || event.status === 'failed') .length; const lastRecoveryRunRecord = this.taskRecovery.getLastRun(); const lastRecoveryRun = lastRecoveryRunRecord ? { runId: lastRecoveryRunRecord.runId, finishedAt: lastRecoveryRunRecord.finishedAt, scanned: lastRecoveryRunRecord.summary.scanned, recoverable: lastRecoveryRunRecord.summary.recoverable, completed: lastRecoveryRunRecord.summary.completed, failed: lastRecoveryRunRecord.summary.failed, unrecoverable: lastRecoveryRunRecord.summary.unrecoverable, } : undefined; return { runningTasks, recoverableTasks, failedTasks, pendingTopics, readyAssets, activeBatchJobs, lastRecoveryRun, storageWarnings, suggestedActions: this.buildActions({ runningTasks, recoverableTasks, failedTasks, pendingTopics, readyAssets, activeBatchJobs, lastRecoveryRun, storageWarnings, }), }; } private buildActions(input: Omit): CreatorDashboardAction[] { const actions: CreatorDashboardAction[] = []; if (input.failedTasks > 0) { actions.push({ id: 'failed-tasks', priority: 'high', title: `处理 ${input.failedTasks} 个失败任务`, description: '先清理失败项,避免批量生产继续堆积不可用结果。', target: 'tasks', }); } if (input.storageWarnings > 0) { actions.push({ id: 'storage-warnings', priority: 'high', title: `检查 ${input.storageWarnings} 条存储回退记录`, description: 'IndexedDB 读写出现过回退或失败,建议先确认任务、批量和素材数据是否完整。', target: 'tasks', payload: { storageAudit: true }, }); } if (input.lastRecoveryRun && ( input.lastRecoveryRun.completed > 0 || input.lastRecoveryRun.failed > 0 || input.lastRecoveryRun.unrecoverable > 0 )) { actions.push({ id: 'last-recovery-run', priority: input.lastRecoveryRun.failed > 0 || input.lastRecoveryRun.unrecoverable > 0 ? 'high' : 'medium', title: `查看最近恢复扫描`, description: `已扫描 ${input.lastRecoveryRun.scanned} 个任务,完成 ${input.lastRecoveryRun.completed} 个,失败 ${input.lastRecoveryRun.failed} 个,不可恢复 ${input.lastRecoveryRun.unrecoverable} 个。`, target: 'tasks', payload: { recoveryRunId: input.lastRecoveryRun.runId }, }); } if (input.recoverableTasks > 0) { actions.push({ id: 'recoverable-tasks', priority: 'high', title: `检查 ${input.recoverableTasks} 个可恢复任务`, description: '有任务仍在等待上游结果,适合优先确认是否已恢复归档。', target: 'tasks', }); } if (input.pendingTopics > 0) { actions.push({ id: 'pending-topics', priority: 'medium', title: `生产 ${input.pendingTopics} 个待用选题`, description: '选题池已有可用方向,可以进入批量生产形成成片。', target: 'batch-production', }); } if (input.readyAssets > 0) { actions.push({ id: 'ready-assets', priority: 'medium', title: `整理 ${input.readyAssets} 条待发布成片`, description: '素材库里有可复用成片,适合补元数据或记录发布表现。', target: 'library', }); } if (input.activeBatchJobs > 0) { actions.push({ id: 'active-batch', priority: 'medium', title: `跟进 ${input.activeBatchJobs} 个批量任务`, description: '批量队列仍有运行、排队或暂停项,需要确认下一步。', target: 'batch-production', }); } if (!actions.length) { actions.push({ id: 'start-topic-video', priority: 'low', title: '开始一条主题生视频', description: '当前没有紧急阻塞,可以从一个新主题直接进入生产。', target: 'topic-to-video', }); } return actions.slice(0, 4); } }