task.routes.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { IncomingMessage, ServerResponse } from 'node:http';
  2. import { createTask, getTask, getAllTasks, runTaskPipeline } from '../services/task-manager.service.ts';
  3. import { generateExcelBuffer } from '../services/export.service.ts';
  4. import { extractTextFromBuffer } from '../utils/file-parser.ts';
  5. import { sendJson, readMultipartFile } from '../utils/http.ts';
  6. import { ingestUploadedCreatorLibrary } from '../services/local-creator-db.service.ts';
  7. /**
  8. * POST /api/tasks/upload-brief
  9. * 上传 Brief 文件,创建任务
  10. */
  11. export async function handleUploadBrief(request: IncomingMessage, response: ServerResponse): Promise<void> {
  12. try {
  13. console.log('[Route] 收到上传请求, Content-Type:', request.headers['content-type']);
  14. const { fileName, buffer } = await readMultipartFile(request);
  15. console.log(`[Route] 解析文件: ${fileName}, 大小: ${buffer.length} bytes`);
  16. if (!buffer || buffer.length === 0) {
  17. sendJson(response, 400, { ok: false, message: '未收到文件内容' });
  18. return;
  19. }
  20. // 提取文件文本
  21. const briefText = extractTextFromBuffer(buffer, fileName);
  22. // 创建任务
  23. const task = createTask(fileName, briefText);
  24. const localCreatorCount = await ingestUploadedCreatorLibrary(buffer, fileName);
  25. console.log(`[Route] 任务已创建: ${task.id}, briefText长度: ${briefText.length}`);
  26. if (localCreatorCount > 0) {
  27. console.log(`[Route] 已从上传文件入库本地达人: ${localCreatorCount} 位`);
  28. }
  29. sendJson(response, 200, {
  30. taskId: task.id,
  31. fileName: task.fileName,
  32. localCreatorCount,
  33. message: `文件 ${fileName} 已上传,任务已创建`,
  34. });
  35. } catch (error) {
  36. console.error('[Upload] Error:', error);
  37. sendJson(response, 500, { ok: false, message: error instanceof Error ? error.message : '上传失败' });
  38. }
  39. }
  40. /**
  41. * POST /api/creator-library/upload
  42. * 上传本地达人资料库文件,入库到 PostgreSQL 清洗达人表
  43. */
  44. export async function handleUploadCreatorLibrary(request: IncomingMessage, response: ServerResponse): Promise<void> {
  45. try {
  46. console.log('[Route] 收到达人资料库上传请求, Content-Type:', request.headers['content-type']);
  47. const { fileName, buffer } = await readMultipartFile(request);
  48. if (!buffer || buffer.length === 0) {
  49. sendJson(response, 400, { ok: false, message: '未收到文件内容' });
  50. return;
  51. }
  52. const localCreatorCount = await ingestUploadedCreatorLibrary(buffer, fileName);
  53. sendJson(response, 200, {
  54. ok: true,
  55. fileName,
  56. localCreatorCount,
  57. message: `达人资料库 ${fileName} 已入库 ${localCreatorCount} 位达人`,
  58. });
  59. } catch (error) {
  60. console.error('[CreatorLibraryUpload] Error:', error);
  61. sendJson(response, 500, { ok: false, message: error instanceof Error ? error.message : '达人资料库上传失败' });
  62. }
  63. }
  64. /**
  65. * POST /api/tasks/:taskId/start
  66. * 启动任务处理流水线(异步执行)
  67. */
  68. export function handleStartTask(request: IncomingMessage, response: ServerResponse, taskId: string): void {
  69. const task = getTask(taskId);
  70. if (!task) {
  71. sendJson(response, 404, { ok: false, message: '任务不存在' });
  72. return;
  73. }
  74. if (task.status !== 'pending') {
  75. sendJson(response, 400, { ok: false, message: `任务状态为 ${task.status},无法启动` });
  76. return;
  77. }
  78. console.log(`[Route] 启动任务: ${taskId}, 当前状态: ${task.status}`);
  79. // 异步执行流水线,不等待完成
  80. runTaskPipeline(taskId).catch((err) => {
  81. console.error(`[Task ${taskId}] Pipeline error:`, err);
  82. });
  83. sendJson(response, 200, { ok: true, message: '任务已启动,请轮询状态' });
  84. }
  85. /**
  86. * GET /api/tasks/:taskId
  87. * 获取任务状态与进度
  88. */
  89. export function handleGetTask(response: ServerResponse, taskId: string): void {
  90. const task = getTask(taskId);
  91. if (!task) {
  92. sendJson(response, 404, { ok: false, message: '任务不存在' });
  93. return;
  94. }
  95. sendJson(response, 200, {
  96. taskId: task.id,
  97. status: task.status,
  98. fileName: task.fileName,
  99. steps: task.steps,
  100. requirements: task.requirements,
  101. searchCriteria: task.searchCriteria,
  102. totalPool: task.totalPool,
  103. candidateCount: task.candidates.length,
  104. strongCount: task.candidates.filter((c) => c.recommendStatus === '强推荐').length,
  105. backupCount: task.candidates.filter((c) => c.recommendStatus === '备选').length,
  106. error: task.error,
  107. createdAt: task.createdAt,
  108. updatedAt: task.updatedAt,
  109. });
  110. }
  111. /**
  112. * GET /api/tasks
  113. * 获取所有任务列表
  114. */
  115. export function handleListTasks(response: ServerResponse): void {
  116. const tasks = getAllTasks().map((t) => ({
  117. taskId: t.id,
  118. status: t.status,
  119. fileName: t.fileName,
  120. candidateCount: t.candidates.length,
  121. createdAt: t.createdAt,
  122. updatedAt: t.updatedAt,
  123. }));
  124. sendJson(response, 200, { tasks });
  125. }
  126. /**
  127. * POST /api/tasks/:taskId/export
  128. * 导出推荐表 Excel 文件
  129. */
  130. export function handleExportTask(response: ServerResponse, taskId: string): void {
  131. const task = getTask(taskId);
  132. if (!task) {
  133. sendJson(response, 404, { ok: false, message: '任务不存在' });
  134. return;
  135. }
  136. if (task.candidates.length === 0) {
  137. sendJson(response, 400, { ok: false, message: '暂无推荐候选人,无法导出' });
  138. return;
  139. }
  140. const excelBuffer = generateExcelBuffer(task.candidates, task.fileName);
  141. const exportFileName = `${task.fileName.replace(/\.[^.]+$/, '')}-推荐名单.xls`;
  142. response.writeHead(200, {
  143. 'Access-Control-Allow-Origin': '*',
  144. 'Content-Type': 'application/vnd.ms-excel; charset=utf-8',
  145. 'Content-Disposition': `attachment; filename="${encodeURIComponent(exportFileName)}"`,
  146. 'Content-Length': excelBuffer.byteLength,
  147. });
  148. response.end(excelBuffer);
  149. }