server.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { createServer, IncomingMessage, ServerResponse } from 'node:http';
  2. type JsonValue = Record<string, unknown> | Array<unknown>;
  3. interface Candidate {
  4. platform: string;
  5. name: string;
  6. fans: string;
  7. price: number;
  8. score: number;
  9. styleMatch: number;
  10. status: string;
  11. reason: string;
  12. risk: string;
  13. }
  14. const port = Number(process.env.TIHAO_AI_API_PORT ?? 4300);
  15. const demoRequirements = [
  16. { label: '客户/品牌', value: '客户A / 新锐护肤品牌', confidence: 96 },
  17. { label: '投放目标', value: '新品种草、真实测评、站内转化蓄水', confidence: 92 },
  18. { label: '平台需求', value: '小红书 20人、抖音 8人、B站 4人', confidence: 90 },
  19. { label: '内容风格', value: '真实体验、通勤生活、轻专业成分党', confidence: 88 }
  20. ];
  21. const demoRules = [
  22. '按 Brief 平台优先提号',
  23. '默认推荐数量不少于需求 3 倍',
  24. '风格匹配低于 80% 自动降级',
  25. '黑名单与低配合度剔除',
  26. '优先近期采集有效价格'
  27. ];
  28. const demoCandidates: Candidate[] = [
  29. {
  30. platform: '小红书',
  31. name: '梨涡测评室',
  32. fans: '18.6万',
  33. price: 6800,
  34. score: 94,
  35. styleMatch: 91,
  36. status: '强推荐',
  37. reason: '近10篇中 9 篇为真实体验或成分拆解,互动中位数高于同价位账号 28%。',
  38. risk: '视频内容偏少,可作为图文主推。'
  39. },
  40. {
  41. platform: '抖音',
  42. name: '林小林认真护肤',
  43. fans: '42.1万',
  44. price: 12800,
  45. score: 87,
  46. styleMatch: 82,
  47. status: '备选',
  48. reason: '爆文率和完播率表现稳定,预算接近上限,建议作为抖音头部备选。',
  49. risk: '单条报价较高,需确认档期。'
  50. }
  51. ];
  52. function sendJson(response: ServerResponse, statusCode: number, payload: JsonValue): void {
  53. const body = JSON.stringify(payload);
  54. response.writeHead(statusCode, {
  55. 'Access-Control-Allow-Origin': '*',
  56. 'Access-Control-Allow-Headers': 'Content-Type',
  57. 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
  58. 'Content-Type': 'application/json; charset=utf-8',
  59. 'Content-Length': Buffer.byteLength(body)
  60. });
  61. response.end(body);
  62. }
  63. function sendExcel(response: ServerResponse, filename: string, candidates: Candidate[]): void {
  64. const rows = candidates
  65. .map(
  66. (candidate) => `
  67. <tr>
  68. <td>${escapeHtml(candidate.platform)}</td>
  69. <td>${escapeHtml(candidate.name)}</td>
  70. <td>${escapeHtml(candidate.fans)}</td>
  71. <td>${candidate.price}</td>
  72. <td>${candidate.score}</td>
  73. <td>${candidate.styleMatch}%</td>
  74. <td>${escapeHtml(candidate.status)}</td>
  75. <td>${escapeHtml(candidate.reason)}</td>
  76. <td>${escapeHtml(candidate.risk)}</td>
  77. </tr>`
  78. )
  79. .join('');
  80. const body = `
  81. <html>
  82. <head>
  83. <meta charset="UTF-8">
  84. <style>
  85. table { border-collapse: collapse; font-family: "Microsoft YaHei", Arial, sans-serif; }
  86. th { background: #1f8a70; color: #fff; }
  87. th, td { border: 1px solid #cfd7d1; padding: 8px 10px; }
  88. td:nth-child(5), td:nth-child(6) { background: #eef7f3; font-weight: 700; }
  89. </style>
  90. </head>
  91. <body>
  92. <table>
  93. <tr>
  94. <th>平台</th><th>账号</th><th>粉丝量</th><th>报价</th><th>综合分</th><th>风格匹配</th><th>推荐状态</th><th>推荐理由</th><th>风险提示</th>
  95. </tr>
  96. ${rows}
  97. </table>
  98. </body>
  99. </html>`;
  100. response.writeHead(200, {
  101. 'Access-Control-Allow-Origin': '*',
  102. 'Content-Type': 'application/vnd.ms-excel; charset=utf-8',
  103. 'Content-Disposition': `attachment; filename="${encodeURIComponent(filename)}"`,
  104. 'Content-Length': Buffer.byteLength(body)
  105. });
  106. response.end(body);
  107. }
  108. function readBody(request: IncomingMessage): Promise<Record<string, unknown>> {
  109. return new Promise((resolve) => {
  110. let body = '';
  111. request.on('data', (chunk) => {
  112. body += chunk;
  113. });
  114. request.on('end', () => {
  115. if (!body) {
  116. resolve({});
  117. return;
  118. }
  119. try {
  120. resolve(JSON.parse(body));
  121. } catch {
  122. resolve({});
  123. }
  124. });
  125. });
  126. }
  127. function escapeHtml(value: string): string {
  128. return value
  129. .replaceAll('&', '&amp;')
  130. .replaceAll('<', '&lt;')
  131. .replaceAll('>', '&gt;')
  132. .replaceAll('"', '&quot;');
  133. }
  134. const server = createServer(async (request, response) => {
  135. const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
  136. if (request.method === 'OPTIONS') {
  137. sendJson(response, 200, { ok: true });
  138. return;
  139. }
  140. if (request.method === 'GET' && requestUrl.pathname === '/api/health') {
  141. sendJson(response, 200, {
  142. ok: true,
  143. name: '提号AI Mock API',
  144. resourcesPath: 'D:\\公司优秀媒体资源库\\4月份资源库'
  145. });
  146. return;
  147. }
  148. if (request.method === 'POST' && requestUrl.pathname === '/api/analyze-brief') {
  149. sendJson(response, 200, {
  150. requirements: demoRequirements,
  151. rules: demoRules,
  152. message: 'Demo 已模拟解析 Brief,真实环境会接入 Excel/Docx 解析与 LLM 抽取。'
  153. });
  154. return;
  155. }
  156. if (request.method === 'POST' && requestUrl.pathname === '/api/recommend') {
  157. sendJson(response, 200, {
  158. candidates: demoCandidates,
  159. totalPool: 104,
  160. message: 'Demo 已模拟按规则生成推荐池。'
  161. });
  162. return;
  163. }
  164. if (request.method === 'POST' && requestUrl.pathname === '/api/rules/learn') {
  165. sendJson(response, 200, {
  166. suggestions: [
  167. {
  168. title: '小红书减少泛美妆号,优先生活方式和通勤场景',
  169. confidence: 93
  170. },
  171. {
  172. title: '抖音预算压低时保留爆文率高账号',
  173. confidence: 87
  174. }
  175. ]
  176. });
  177. return;
  178. }
  179. if (request.method === 'POST' && requestUrl.pathname === '/api/recommendations/export') {
  180. const body = await readBody(request);
  181. const candidates = Array.isArray(body['candidates'])
  182. ? (body['candidates'] as Candidate[])
  183. : demoCandidates;
  184. sendExcel(response, '客户A-新锐护肤品牌-需求推荐名单.xls', candidates);
  185. return;
  186. }
  187. sendJson(response, 404, { ok: false, message: 'Not found' });
  188. });
  189. server.listen(port, () => {
  190. console.log(`Tihao AI mock API listening on http://localhost:${port}`);
  191. });