Files
Brain/brain-h5/server.mjs
MingNian b0fce840a3 首次提交:脑晴测 H5 项目
包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、
Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。

技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。
DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
2026-07-28 10:24:47 +08:00

149 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)"}`);
});