task-manager.service.ts 6.7 KB

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