包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、 Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。 技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。 DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
149 lines
6.0 KiB
JavaScript
149 lines
6.0 KiB
JavaScript
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)"}`);
|
||
});
|