task-manager.service.ts 6.3 KB

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