import { createServer } from 'node:http'; import { config } from './config.ts'; import { sendJson, readBody, extractPathParam, extractPathTail } from './utils/http.ts'; import { handleUploadBrief, handleStartTask, handleGetTask, handleListTasks, handleExportTask, } from './routes/task.routes.ts'; // ─── Legacy demo data (保留向下兼容) ────────────────────────── interface LegacyCandidate { platform: string; name: string; fans: string; price: number; score: number; styleMatch: number; status: string; reason: string; risk: string; } const demoRequirements = [ { label: '客户/品牌', value: '客户A / 新锐护肤品牌', confidence: 96 }, { label: '投放目标', value: '新品种草、真实测评、站内转化蓄水', confidence: 92 }, { label: '平台需求', value: '小红书 20人、抖音 8人、B站 4人', confidence: 90 }, { label: '内容风格', value: '真实体验、通勤生活、轻专业成分党', confidence: 88 }, ]; const demoRules = [ '按 Brief 平台优先提号', '默认推荐数量不少于需求 3 倍', '风格匹配低于 80% 自动降级', '黑名单与低配合度剔除', '优先近期采集有效价格', ]; const demoCandidates: LegacyCandidate[] = [ { platform: '小红书', name: '梨涡测评室', fans: '18.6万', price: 6800, score: 94, styleMatch: 91, status: '强推荐', reason: '近10篇中 9 篇为真实体验或成分拆解,互动中位数高于同价位账号 28%。', risk: '视频内容偏少,可作为图文主推。', }, { platform: '抖音', name: '林小林认真护肤', fans: '42.1万', price: 12800, score: 87, styleMatch: 82, status: '备选', reason: '爆文率和完播率表现稳定,预算接近上限,建议作为抖音头部备选。', risk: '单条报价较高,需确认档期。', }, ]; function escapeHtml(value: string): string { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"'); } function sendExcel(response: import('node:http').ServerResponse, filename: string, candidates: LegacyCandidate[]): void { const rows = candidates .map( (candidate) => ` ${escapeHtml(candidate.platform)} ${escapeHtml(candidate.name)} ${escapeHtml(candidate.fans)} ${candidate.price} ${candidate.score} ${candidate.styleMatch}% ${escapeHtml(candidate.status)} ${escapeHtml(candidate.reason)} ${escapeHtml(candidate.risk)} ` ) .join(''); const body = ` ${rows}
平台账号粉丝量报价综合分风格匹配推荐状态推荐理由风险提示
`; response.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/vnd.ms-excel; charset=utf-8', 'Content-Disposition': `attachment; filename="${encodeURIComponent(filename)}"`, 'Content-Length': Buffer.byteLength(body), }); response.end(body); } // ─── HTTP Server ────────────────────────────────────────────── const server = createServer(async (request, response) => { const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`); const { pathname } = requestUrl; // CORS preflight if (request.method === 'OPTIONS') { sendJson(response, 200, { ok: true }); return; } // ── 新接口:任务流水线 ──────────────────────────── // POST /api/tasks/upload-brief - 上传 Brief if (request.method === 'POST' && pathname === '/api/tasks/upload-brief') { await handleUploadBrief(request, response); return; } // GET /api/tasks - 任务列表 if (request.method === 'GET' && pathname === '/api/tasks') { handleListTasks(response); return; } // GET /api/tasks/:taskId - 任务详情/进度 if (request.method === 'GET' && pathname.startsWith('/api/tasks/')) { const taskId = extractPathParam(pathname, '/api/tasks/'); if (taskId) { handleGetTask(response, taskId); return; } } // POST /api/tasks/:taskId/start - 启动任务 if (request.method === 'POST' && pathname.match(/^\/api\/tasks\/[^/]+\/start$/)) { const taskId = extractPathParam(pathname, '/api/tasks/'); handleStartTask(request, response, taskId); return; } // POST /api/tasks/:taskId/export - 导出推荐表 if (request.method === 'POST' && pathname.match(/^\/api\/tasks\/[^/]+\/export$/)) { const tail = extractPathTail(pathname, '/api/tasks/'); const taskId = tail.replace('/export', ''); handleExportTask(response, taskId); return; } // ── 旧接口:保留向下兼容 ────────────────────────── if (request.method === 'GET' && pathname === '/api/health') { sendJson(response, 200, { ok: true, name: '提号AI API', resourcesPath: 'D:\\公司优秀媒体资源库\\4月份资源库', version: '2.0.0', features: ['task-pipeline', 'brief-upload', 'justone-api', 'tikhub-api', 'export'], }); return; } if (request.method === 'POST' && pathname === '/api/analyze-brief') { sendJson(response, 200, { requirements: demoRequirements, rules: demoRules, message: 'Demo 已模拟解析 Brief,真实环境会接入 Excel/Docx 解析与 LLM 抽取。', }); return; } if (request.method === 'POST' && pathname === '/api/recommend') { sendJson(response, 200, { candidates: demoCandidates, totalPool: 104, message: 'Demo 已模拟按规则生成推荐池。', }); return; } if (request.method === 'POST' && pathname === '/api/rules/learn') { sendJson(response, 200, { suggestions: [ { title: '小红书减少泛美妆号,优先生活方式和通勤场景', confidence: 93 }, { title: '抖音预算压低时保留爆文率高账号', confidence: 87 }, ], }); return; } if (request.method === 'POST' && pathname === '/api/recommendations/export') { const body = await readBody(request); const candidates = Array.isArray(body['candidates']) ? (body['candidates'] as LegacyCandidate[]) : demoCandidates; sendExcel(response, '客户A-新锐护肤品牌-需求推荐名单.xls', candidates); return; } sendJson(response, 404, { ok: false, message: 'Not found' }); }); server.listen(config.port, () => { console.log(`提号AI API listening on http://localhost:${config.port}`); console.log('新接口:'); console.log(' POST /api/tasks/upload-brief - 上传 Brief 文件'); console.log(' POST /api/tasks/:id/start - 启动任务处理'); console.log(' GET /api/tasks/:id - 查询任务进度'); console.log(' GET /api/tasks - 任务列表'); console.log(' POST /api/tasks/:id/export - 导出推荐表'); console.log('旧接口: /api/health, /api/analyze-brief, /api/recommend, /api/rules/learn, /api/recommendations/export'); });