import { createServer } from "node:http"; import { readFile, stat } from "node:fs/promises"; import { extname, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; const PORT = Number(process.env.PORT) || 4173; const HOST = process.env.HOST || "0.0.0.0"; const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ""; const CLIENT_DIR = join(dirname(fileURLToPath(import.meta.url)), "dist", "client"); const SYSTEM_PROMPT = `你是一名数字脑健康表现报告助手。根据用户完成游戏时产生的结构化数据,生成通俗、克制、非医疗化的中文报告。不得诊断疾病,不得推荐药物或医疗处置,不得使用"异常、受损、提示存在问题或挑战"等暗示医学结论的说法。只描述本次表现,优先使用"反应相对偏慢、注意表现有波动、容易遗漏、仍有提升空间"。只输出合法 JSON:{"summary":"80字内总结","strength":"60字内相对优势","opportunity":"60字内提升空间","recommendations":["建议1","建议2","建议3"]}`; const MIME = { ".html": "text/html; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf", ".eot": "application/vnd.ms-fontobject", ".map": "application/json; charset=utf-8", }; function send(response, status, body, headers = {}) { response.writeHead(status, headers); response.end(body); } function sendJson(response, status, payload) { send(response, status, JSON.stringify(payload), { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", }); } async function serveStatic(request, response, pathname) { const safe = normalize(pathname).replace(/^(\.\.[/\\])+/, ""); let filePath = join(CLIENT_DIR, safe); let info; try { info = await stat(filePath); } catch { if (request.headers.get?.("accept")?.includes("text/html") || (request.headers.accept || "").includes("text/html")) { try { const index = await readFile(join(CLIENT_DIR, "index.html")); send(response, 200, index, { "content-type": "text/html; charset=utf-8" }); return; } catch { send(response, 404, "Not Found", { "content-type": "text/plain; charset=utf-8" }); return; } } send(response, 404, "Not Found", { "content-type": "text/plain; charset=utf-8" }); return; } if (info.isDirectory()) { filePath = join(filePath, "index.html"); try { info = await stat(filePath); } catch { send(response, 404, "Not Found"); return; } } const data = await readFile(filePath); const mime = MIME[extname(filePath).toLowerCase()] || "application/octet-stream"; const headers = { "content-type": mime }; if (extname(filePath).match(/\.(js|css|woff|woff2|png|jpg|jpeg|gif|webp|svg|ico)$/)) { headers["cache-control"] = "public, max-age=31536000, immutable"; } send(response, 200, data, headers); } async function readBody(request) { const chunks = []; for await (const chunk of request) chunks.push(chunk); return Buffer.concat(chunks).toString("utf-8"); } async function analyze(request, response) { if (!DEEPSEEK_API_KEY) { sendJson(response, 503, { error: "AI service is not configured. Set DEEPSEEK_API_KEY env var." }); return; } try { const raw = await readBody(request); const assessment = JSON.parse(raw || "{}"); const upstream = await fetch("https://api.deepseek.com/chat/completions", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${DEEPSEEK_API_KEY}` }, body: JSON.stringify({ model: "deepseek-chat", temperature: 0.35, response_format: { type: "json_object" }, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: JSON.stringify({ profile: assessment.profile, metrics: assessment.metrics, eventCount: assessment.events?.length ?? 0 }) }, ], }), signal: AbortSignal.timeout(15000), }); if (!upstream.ok) { sendJson(response, 502, { error: "AI service request failed" }); return; } const payload = await upstream.json(); const content = payload?.choices?.[0]?.message?.content; const rawReport = JSON.parse(String(content || "{}").replace(/^```json\s*|\s*```$/g, "")); const parsed = { summary: String(rawReport.summary || "本次四项任务已经完成。"), strength: String(rawReport.strength || "部分任务表现相对稳定。"), opportunity: String(rawReport.opportunity || "仍有可以通过规律练习提升的空间。"), recommendations: Array.isArray(rawReport.recommendations) ? rawReport.recommendations.slice(0, 3).map(String) : [], }; sendJson(response, 200, parsed); } catch (error) { sendJson(response, 502, { error: error?.message || "AI report could not be generated" }); } } const server = createServer(async (request, response) => { const url = new URL(request.url, `http://${request.headers.host || "localhost"}`); try { if (url.pathname === "/api/analyze") { if (request.method !== "POST") { send(response, 405, "Method not allowed"); return; } await analyze(request, response); return; } if (!["GET", "HEAD"].includes(request.method)) { send(response, 405, "Method not allowed"); return; } await serveStatic(request, response, url.pathname); } catch (error) { if (!response.headersSent) sendJson(response, 500, { error: error?.message || "Internal error" }); } }); server.listen(PORT, HOST, () => { console.log(`brain-h5 server listening on http://${HOST}:${PORT}`); console.log(`static dir: ${CLIENT_DIR}`); console.log(`DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY ? "configured" : "MISSING (set DEEPSEEK_API_KEY env var)"}`); });