task-manager.service.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import { analyzeBrief } from './llm.service.ts';
  2. import { generateRecommendations, type NormalizedCandidate } from './recommendation.service.ts';
  3. export type TaskStatus = 'pending' | 'uploading' | 'analyzing' | 'searching' | 'processing' | 'exporting' | 'completed' | 'failed';
  4. export type StepStatus = 'pending' | 'running' | 'done' | 'error';
  5. export interface TaskStep {
  6. id: string;
  7. label: string;
  8. status: StepStatus;
  9. progress: number;
  10. detail?: string;
  11. }
  12. export interface Task {
  13. id: string;
  14. status: TaskStatus;
  15. fileName: string;
  16. briefText: string;
  17. steps: TaskStep[];
  18. requirements: Array<{ label: string; value: string; confidence: number }>;
  19. searchCriteria: {
  20. platforms: string[];
  21. keywords: string[];
  22. fanRange: { min: number; max: number };
  23. budgetRange: { min: number; max: number };
  24. region?: string;
  25. gender?: string;
  26. contentTags?: string[];
  27. excludeTags?: string[];
  28. targetCount?: number;
  29. evaluationNeeds?: {
  30. cpmCpe?: boolean;
  31. commercialStability?: boolean;
  32. recentPerformance?: boolean;
  33. updateFrequency?: boolean;
  34. commentQuality?: boolean;
  35. publicSentiment?: boolean;
  36. audienceGender?: boolean;
  37. reasons?: string[];
  38. };
  39. } | null;
  40. candidates: NormalizedCandidate[];
  41. totalPool: number;
  42. error?: string;
  43. createdAt: string;
  44. updatedAt: string;
  45. }
  46. const tasks = new Map<string, Task>();
  47. function generateId(): string {
  48. return `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  49. }
  50. function defaultSteps(): TaskStep[] {
  51. return [
  52. { id: 'upload', label: '上传文件', status: 'pending', progress: 0 },
  53. { id: 'analyze', label: 'AI 解析 Brief 需求', status: 'pending', progress: 0 },
  54. { id: 'rules', label: '确定筛选规则', status: 'pending', progress: 0 },
  55. { id: 'search', label: '搜索达人资源', status: 'pending', progress: 0 },
  56. { id: 'recommend', label: '生成推荐名单', status: 'pending', progress: 0 },
  57. { id: 'export', label: '准备导出', status: 'pending', progress: 0 },
  58. ];
  59. }
  60. export function createTask(fileName: string, briefText: string): Task {
  61. const task: Task = {
  62. id: generateId(),
  63. status: 'pending',
  64. fileName,
  65. briefText,
  66. steps: defaultSteps(),
  67. requirements: [],
  68. searchCriteria: null,
  69. candidates: [],
  70. totalPool: 0,
  71. createdAt: new Date().toISOString(),
  72. updatedAt: new Date().toISOString(),
  73. };
  74. tasks.set(task.id, task);
  75. return task;
  76. }
  77. export function getTask(taskId: string): Task | undefined {
  78. return tasks.get(taskId);
  79. }
  80. export function getAllTasks(): Task[] {
  81. return Array.from(tasks.values());
  82. }
  83. function updateStep(task: Task, stepId: string, updates: Partial<TaskStep>): void {
  84. const step = task.steps.find((s) => s.id === stepId);
  85. if (step) {
  86. Object.assign(step, updates);
  87. }
  88. task.updatedAt = new Date().toISOString();
  89. }
  90. /**
  91. * 执行完整的提号流程
  92. * Upload Brief → AI解析 → 规则确定 → 搜索达人 → 生成推荐 → 准备导出
  93. */
  94. export async function runTaskPipeline(taskId: string): Promise<void> {
  95. const task = tasks.get(taskId);
  96. if (!task) {
  97. throw new Error(`Task not found: ${taskId}`);
  98. }
  99. try {
  100. // Step 1: Upload (already done)
  101. task.status = 'analyzing';
  102. updateStep(task, 'upload', { status: 'done', progress: 100, detail: `已上传 ${task.fileName}` });
  103. // Step 2: AI 解析 Brief
  104. updateStep(task, 'analyze', { status: 'running', progress: 20, detail: '正在解析 Brief 需求...' });
  105. console.log(`[TaskPipeline] Brief 文本长度: ${task.briefText.length} 字符`);
  106. console.log(`[TaskPipeline] Brief 前200字: ${task.briefText.substring(0, 200)}...`);
  107. const analysisResult = await analyzeBrief(task.briefText);
  108. task.requirements = analysisResult.requirements;
  109. task.searchCriteria = analysisResult.searchCriteria;
  110. console.log(`[TaskPipeline] LLM 提取需求数: ${analysisResult.requirements.length}`);
  111. console.log(`[TaskPipeline] 搜索条件 platforms:`, analysisResult.searchCriteria.platforms);
  112. console.log(`[TaskPipeline] 搜索条件 keywords:`, analysisResult.searchCriteria.keywords);
  113. console.log(`[TaskPipeline] 搜索条件 fanRange:`, analysisResult.searchCriteria.fanRange);
  114. console.log(`[TaskPipeline] 搜索条件 budgetRange:`, analysisResult.searchCriteria.budgetRange);
  115. updateStep(task, 'analyze', {
  116. status: 'done',
  117. progress: 100,
  118. detail: `提取 ${analysisResult.requirements.length} 项需求,${analysisResult.searchCriteria.platforms.length} 个平台`,
  119. });
  120. // Step 3: 确定规则
  121. task.status = 'searching';
  122. updateStep(task, 'rules', { status: 'running', progress: 50, detail: '应用默认筛选规则...' });
  123. // 规则确定是自动的(使用默认规则)
  124. await delay(500);
  125. updateStep(task, 'rules', { status: 'done', progress: 100, detail: '已应用 5 条默认规则' });
  126. // Step 4 & 5: 搜索达人 + 生成推荐
  127. updateStep(task, 'search', { status: 'running', progress: 10, detail: '正在搜索达人资源...' });
  128. const candidates = await generateRecommendations(
  129. task.searchCriteria,
  130. (stage, detail) => {
  131. if (stage === 'search') {
  132. updateStep(task, 'search', { status: 'running', progress: 50, detail });
  133. } else if (stage === 'processing') {
  134. updateStep(task, 'search', { status: 'done', progress: 100, detail: '搜索完成' });
  135. updateStep(task, 'recommend', { status: 'running', progress: 50, detail });
  136. }
  137. }
  138. );
  139. task.candidates = candidates;
  140. task.totalPool = candidates.length;
  141. console.log(`[TaskPipeline] 推荐引擎返回 ${candidates.length} 位候选达人`);
  142. updateStep(task, 'search', { status: 'done', progress: 100, detail: `召回 ${candidates.length} 位候选达人` });
  143. updateStep(task, 'recommend', {
  144. status: 'done',
  145. progress: 100,
  146. detail: `强推荐 ${candidates.filter((c) => c.recommendStatus === '强推荐').length} 位,备选 ${candidates.filter((c) => c.recommendStatus === '备选').length} 位`,
  147. });
  148. // Step 6: 准备导出
  149. task.status = 'completed';
  150. updateStep(task, 'export', { status: 'done', progress: 100, detail: '推荐表已准备就绪,可导出' });
  151. } catch (error) {
  152. task.status = 'failed';
  153. task.error = error instanceof Error ? error.message : '未知错误';
  154. // Mark current running step as error
  155. for (const step of task.steps) {
  156. if (step.status === 'running') {
  157. step.status = 'error';
  158. step.detail = task.error;
  159. }
  160. }
  161. }
  162. }
  163. function delay(ms: number): Promise<void> {
  164. return new Promise((resolve) => setTimeout(resolve, ms));
  165. }