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) => `
| ${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;
}
// 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');
});