| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- import { createServer } from 'node:http';
- import { config } from './config.ts';
- import { sendJson, readBody, extractPathParam, extractPathTail } from './utils/http.ts';
- import {
- handleUploadBrief,
- handleUploadCreatorLibrary,
- 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) => `
- <tr>
- <td>${escapeHtml(candidate.platform)}</td>
- <td>${escapeHtml(candidate.name)}</td>
- <td>${escapeHtml(candidate.fans)}</td>
- <td>${candidate.price}</td>
- <td>${candidate.score}</td>
- <td>${candidate.styleMatch}%</td>
- <td>${escapeHtml(candidate.status)}</td>
- <td>${escapeHtml(candidate.reason)}</td>
- <td>${escapeHtml(candidate.risk)}</td>
- </tr>`
- )
- .join('');
- const body = `
- <html>
- <head>
- <meta charset="UTF-8">
- <style>
- table { border-collapse: collapse; font-family: "Microsoft YaHei", Arial, sans-serif; }
- th { background: #1f8a70; color: #fff; }
- th, td { border: 1px solid #cfd7d1; padding: 8px 10px; }
- td:nth-child(5), td:nth-child(6) { background: #eef7f3; font-weight: 700; }
- </style>
- </head>
- <body>
- <table>
- <tr>
- <th>平台</th><th>账号</th><th>粉丝量</th><th>报价</th><th>综合分</th><th>风格匹配</th><th>推荐状态</th><th>推荐理由</th><th>风险提示</th>
- </tr>
- ${rows}
- </table>
- </body>
- </html>`;
- 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;
- }
- // POST /api/creator-library/upload - 上传本地达人资料库
- if (request.method === 'POST' && pathname === '/api/creator-library/upload') {
- await handleUploadCreatorLibrary(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');
- });
|