538de50bb1
- Prisma User 模型新增 username 字段(唯一索引) - 注册先查重复再创建,返回友好中文提示(非 500) - 登录支持用户名/手机号/邮箱三种方式 - 前端注册表单增加用户名输入框,预校验 2-20 位格式 - 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx - 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步 - AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构 - 新增 UserAiAssistantService(18 工具)+ AiAssistantController - admin 助手新增 search + mark-all-notifications-read 工具 - 修复注册 500 错误:catch Prisma P2002 → BadRequestException - Baidu Analytics Script 注入 root layout
93 lines
2.4 KiB
JavaScript
Executable File
93 lines
2.4 KiB
JavaScript
Executable File
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const ROOT = path.join(__dirname, 'out');
|
|
|
|
const MIME = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.svg': 'image/svg+xml',
|
|
'.ico': 'image/x-icon',
|
|
'.webp': 'image/webp',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2',
|
|
'.ttf': 'font/ttf',
|
|
'.txt': 'text/plain; charset=utf-8',
|
|
'.xml': 'application/xml; charset=utf-8',
|
|
'.webmanifest': 'application/manifest+json',
|
|
'.map': 'application/octet-stream',
|
|
};
|
|
|
|
function resolvePath(url) {
|
|
const decoded = decodeURIComponent(url).split('?')[0];
|
|
if (decoded === '/') return path.join(ROOT, 'index.html');
|
|
|
|
const ext = path.extname(decoded);
|
|
if (ext) return path.join(ROOT, decoded);
|
|
|
|
const asHtml = path.join(ROOT, decoded + '.html');
|
|
if (fs.existsSync(asHtml)) return asHtml;
|
|
|
|
const asIndex = path.join(ROOT, decoded, 'index.html');
|
|
if (fs.existsSync(asIndex)) return asIndex;
|
|
|
|
return path.join(ROOT, decoded + '.html');
|
|
}
|
|
|
|
function sendFile(res, filePath, statusCode) {
|
|
const ext = path.extname(filePath);
|
|
const ct = MIME[ext] || 'application/octet-stream';
|
|
const isHtml = ext === '.html';
|
|
|
|
fs.readFile(filePath, (err, data) => {
|
|
if (err) {
|
|
res.writeHead(500);
|
|
res.end('Internal Server Error');
|
|
return;
|
|
}
|
|
res.writeHead(statusCode, {
|
|
'Content-Type': ct,
|
|
'Cache-Control': isHtml ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
});
|
|
res.end(data);
|
|
});
|
|
}
|
|
|
|
function serve(req, res) {
|
|
const filePath = path.normalize(resolvePath(req.url));
|
|
|
|
if (!filePath.startsWith(ROOT)) {
|
|
res.writeHead(403);
|
|
res.end('Forbidden');
|
|
return;
|
|
}
|
|
|
|
fs.access(filePath, fs.constants.F_OK, (err) => {
|
|
if (err) {
|
|
const four04 = path.join(ROOT, '404.html');
|
|
fs.access(four04, fs.constants.F_OK, (err2) => {
|
|
if (err2) {
|
|
sendFile(res, path.join(ROOT, 'index.html'), 200);
|
|
} else {
|
|
sendFile(res, four04, 404);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
sendFile(res, filePath, 200);
|
|
});
|
|
}
|
|
|
|
const server = http.createServer(serve);
|
|
server.listen(PORT, () => {
|
|
console.log(`Static server running at http://localhost:${PORT} (serving ${ROOT})`);
|
|
});
|