creator-dashboard.service.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { Injectable } from '@angular/core';
  2. import { CreatorDashboardAction, CreatorDashboardSummary } from '../models/creator-dashboard.model';
  3. import { BatchJobService } from './batch-job.service';
  4. import { ContentAssetService } from './content-asset.service';
  5. import { GenerationTaskService } from './generation-task.service';
  6. import { IndexedDbJsonStorage } from './indexed-db-json-storage';
  7. import { TaskRecoveryService } from './task-recovery.service';
  8. import { TopicPoolService } from './topic-pool.service';
  9. @Injectable({ providedIn: 'root' })
  10. export class CreatorDashboardService {
  11. constructor(
  12. private generationTasks: GenerationTaskService,
  13. private topicPool: TopicPoolService,
  14. private contentAssets: ContentAssetService,
  15. private batchJobs: BatchJobService,
  16. private taskRecovery: TaskRecoveryService,
  17. ) {}
  18. getSummary(): CreatorDashboardSummary {
  19. const tasks = this.generationTasks.tasks;
  20. const topics = this.topicPool.listTopics();
  21. const assets = this.contentAssets.assets;
  22. const jobs = this.batchJobs.listJobs();
  23. const runningTasks = tasks.filter((task) => task.status === 'running' || task.status === 'waiting_external').length;
  24. const recoverableTasks = tasks.filter((task) => task.recoverable && task.status !== 'completed' && task.status !== 'cancelled').length;
  25. const failedTasks = tasks.filter((task) => task.status === 'failed').length;
  26. const pendingTopics = topics.filter((topic) => topic.status === 'idea' || topic.status === 'script_ready').length;
  27. const readyAssets = assets.filter((asset) => asset.type === 'video' && asset.source?.isFinal && !asset.archived).length;
  28. const activeBatchJobs = jobs.filter((job) => job.status === 'queued' || job.status === 'running' || job.status === 'paused').length;
  29. const storageWarnings = IndexedDbJsonStorage.getAuditTrail()
  30. .filter((event) => event.status === 'fallback' || event.status === 'failed')
  31. .length;
  32. const lastRecoveryRunRecord = this.taskRecovery.getLastRun();
  33. const lastRecoveryRun = lastRecoveryRunRecord
  34. ? {
  35. runId: lastRecoveryRunRecord.runId,
  36. finishedAt: lastRecoveryRunRecord.finishedAt,
  37. scanned: lastRecoveryRunRecord.summary.scanned,
  38. recoverable: lastRecoveryRunRecord.summary.recoverable,
  39. completed: lastRecoveryRunRecord.summary.completed,
  40. failed: lastRecoveryRunRecord.summary.failed,
  41. unrecoverable: lastRecoveryRunRecord.summary.unrecoverable,
  42. }
  43. : undefined;
  44. return {
  45. runningTasks,
  46. recoverableTasks,
  47. failedTasks,
  48. pendingTopics,
  49. readyAssets,
  50. activeBatchJobs,
  51. lastRecoveryRun,
  52. storageWarnings,
  53. suggestedActions: this.buildActions({
  54. runningTasks,
  55. recoverableTasks,
  56. failedTasks,
  57. pendingTopics,
  58. readyAssets,
  59. activeBatchJobs,
  60. lastRecoveryRun,
  61. storageWarnings,
  62. }),
  63. };
  64. }
  65. private buildActions(input: Omit<CreatorDashboardSummary, 'suggestedActions'>): CreatorDashboardAction[] {
  66. const actions: CreatorDashboardAction[] = [];
  67. if (input.failedTasks > 0) {
  68. actions.push({
  69. id: 'failed-tasks',
  70. priority: 'high',
  71. title: `处理 ${input.failedTasks} 个失败任务`,
  72. description: '先清理失败项,避免批量生产继续堆积不可用结果。',
  73. target: 'tasks',
  74. });
  75. }
  76. if (input.storageWarnings > 0) {
  77. actions.push({
  78. id: 'storage-warnings',
  79. priority: 'high',
  80. title: `检查 ${input.storageWarnings} 条存储回退记录`,
  81. description: 'IndexedDB 读写出现过回退或失败,建议先确认任务、批量和素材数据是否完整。',
  82. target: 'tasks',
  83. payload: { storageAudit: true },
  84. });
  85. }
  86. if (input.lastRecoveryRun && (
  87. input.lastRecoveryRun.completed > 0
  88. || input.lastRecoveryRun.failed > 0
  89. || input.lastRecoveryRun.unrecoverable > 0
  90. )) {
  91. actions.push({
  92. id: 'last-recovery-run',
  93. priority: input.lastRecoveryRun.failed > 0 || input.lastRecoveryRun.unrecoverable > 0 ? 'high' : 'medium',
  94. title: `查看最近恢复扫描`,
  95. description: `已扫描 ${input.lastRecoveryRun.scanned} 个任务,完成 ${input.lastRecoveryRun.completed} 个,失败 ${input.lastRecoveryRun.failed} 个,不可恢复 ${input.lastRecoveryRun.unrecoverable} 个。`,
  96. target: 'tasks',
  97. payload: { recoveryRunId: input.lastRecoveryRun.runId },
  98. });
  99. }
  100. if (input.recoverableTasks > 0) {
  101. actions.push({
  102. id: 'recoverable-tasks',
  103. priority: 'high',
  104. title: `检查 ${input.recoverableTasks} 个可恢复任务`,
  105. description: '有任务仍在等待上游结果,适合优先确认是否已恢复归档。',
  106. target: 'tasks',
  107. });
  108. }
  109. if (input.pendingTopics > 0) {
  110. actions.push({
  111. id: 'pending-topics',
  112. priority: 'medium',
  113. title: `生产 ${input.pendingTopics} 个待用选题`,
  114. description: '选题池已有可用方向,可以进入批量生产形成成片。',
  115. target: 'batch-production',
  116. });
  117. }
  118. if (input.readyAssets > 0) {
  119. actions.push({
  120. id: 'ready-assets',
  121. priority: 'medium',
  122. title: `整理 ${input.readyAssets} 条待发布成片`,
  123. description: '素材库里有可复用成片,适合补元数据或记录发布表现。',
  124. target: 'library',
  125. });
  126. }
  127. if (input.activeBatchJobs > 0) {
  128. actions.push({
  129. id: 'active-batch',
  130. priority: 'medium',
  131. title: `跟进 ${input.activeBatchJobs} 个批量任务`,
  132. description: '批量队列仍有运行、排队或暂停项,需要确认下一步。',
  133. target: 'batch-production',
  134. });
  135. }
  136. if (!actions.length) {
  137. actions.push({
  138. id: 'start-topic-video',
  139. priority: 'low',
  140. title: '开始一条主题生视频',
  141. description: '当前没有紧急阻塞,可以从一个新主题直接进入生产。',
  142. target: 'topic-to-video',
  143. });
  144. }
  145. return actions.slice(0, 4);
  146. }
  147. }