chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
import express from "express";
import JSONbig from "json-bigint";
import swaggerUi from "swagger-ui-express";
import swaggerSpecs from "./config/swagger.js";
import config from "./appConfig/index.js";
import { log, logger } from "./utils/log/logUtils.js";
import logCacheManager from "./utils/log/logCacheManager.js";
import { errorHandler, notFoundHandler } from "./utils/error/errorHandler.js";
import router from "./routes/router.js";
import { cleanupInitProjectOnStartup } from "./utils/project/initProjectCleanupUtils.js";
import { startScheduler, stopScheduler } from "./scheduler/pnpmPruneScheduler.js";
import path from "path";
const app = express();
// 解析 JSON 请求体,使用 json-bigint 处理大整数
app.use(
express.json({
limit: config.REQUEST_BODY_LIMIT,
reviver: (key, value) => {
// 对于大整数,保持为字符串
if (typeof value === "number" && !Number.isSafeInteger(value)) {
return value.toString();
}
return value;
},
})
);
// 解析 URL 编码的请求体
app.use(
express.urlencoded({ extended: true, limit: config.REQUEST_BODY_LIMIT })
);
// 使用日志中间件
app.use(logger);
// 帮助方法:对 URL 路径做尽可能多次解码(处理客户端双重编码等情况)
const safeDecodePath = (p) => {
let prev = p;
try {
// 连续解码直到不再变化或解码失败
while (true) {
const decoded = decodeURIComponent(prev);
if (decoded === prev) break;
prev = decoded;
}
} catch (e) {
// 非法编码时直接返回当前结果,避免抛错
}
return prev;
};
// 静态文件服务:提供页面工程文件的直接访问
// 格式1: /api/page/static/<projectId>/<path/to/file> -> config.PROJECT_SOURCE_DIR
app.use("/api/page/static/:projectId", (req, res, next) => {
const { projectId } = req.params;
// req.path 是挂载点之后的路径,形如 "/path/to/file"
let filePath = req.path || "/";
if (!projectId || filePath === "/") {
return res.status(404).send("Not Found");
}
// 设置 CORS 头,允许跨域访问静态资源(必须在 sendFile 之前设置)
const origin = req.headers.origin;
// 如果请求包含 Origin使用具体的 origin否则使用 *
// 注意:当使用 credentials 时,不能使用 *,必须使用具体的 origin
const allowOrigin = origin || "*";
res.header("Access-Control-Allow-Origin", allowOrigin);
res.header("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,DELETE,OPTIONS");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Fragment");
res.header("Access-Control-Expose-Headers", "Content-Type");
if (origin) {
res.header("Access-Control-Allow-Credentials", "true");
res.header("Vary", "Origin");
}
// 处理 OPTIONS 预检请求
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
// 去掉前导 "/",转成相对路径
filePath = filePath.replace(/^\/+/, "");
// 对可能被多次编码的路径做安全解码(支持中文等)
const decodedPath = safeDecodePath(filePath);
const fullPath = path.join(config.PROJECT_SOURCE_DIR, projectId, decodedPath);
// 使用 headers 选项确保 CORS 头被保留
const corsHeaders = {
"Access-Control-Allow-Origin": allowOrigin,
"Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Fragment",
};
if (origin) {
corsHeaders["Access-Control-Allow-Credentials"] = "true";
corsHeaders["Vary"] = "Origin";
}
return res.sendFile(
fullPath,
{
// dotfiles: "ignore",
dotfiles: "allow",
headers: corsHeaders,
},
(err) => {
if (err) return next();
}
);
});
// 静态文件服务:提供桌面文件的直接访问
// 格式2: /api/computer/static/<userId>/<cId>/<path/to/file> -> config.COMPUTER_WORKSPACE_DIR
app.use("/api/computer/static/:userId/:cId", (req, res, next) => {
const { userId, cId } = req.params;
let filePath = req.path || "/";
if (!userId || !cId || filePath === "/") {
return res.status(404).send("Not Found");
}
// 设置 CORS 头,允许跨域访问静态资源(必须在 sendFile 之前设置)
const origin = req.headers.origin;
// 如果请求包含 Origin使用具体的 origin否则使用 *
// 注意:当使用 credentials 时,不能使用 *,必须使用具体的 origin
const allowOrigin = origin || "*";
res.header("Access-Control-Allow-Origin", allowOrigin);
res.header("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,DELETE,OPTIONS");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Range, If-Range");
res.header("Access-Control-Expose-Headers", "Content-Type, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified");
if (origin) {
res.header("Access-Control-Allow-Credentials", "true");
res.header("Vary", "Origin");
}
// 处理 OPTIONS 预检请求
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
// 去掉前导 "/",确保是相对路径
filePath = filePath.replace(/^\/+/, "");
// 安全多次解码,解决 "%25E4%25BD%25A0" 这种双重编码的中文
const decodedPath = safeDecodePath(filePath);
const fullPath = path.join(
config.COMPUTER_WORKSPACE_DIR,
userId,
cId,
decodedPath
);
// 使用 headers 选项确保 CORS 头被保留
const corsHeaders = {
"Access-Control-Allow-Origin": allowOrigin,
"Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Range, If-Range",
"Access-Control-Expose-Headers": "Content-Type, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified",
};
if (origin) {
corsHeaders["Access-Control-Allow-Credentials"] = "true";
corsHeaders["Vary"] = "Origin";
}
return res.sendFile(
fullPath,
{
// dotfiles: "ignore",
dotfiles: "allow",
headers: corsHeaders,
},
(err) => {
if (err) return next();
}
);
});
// Swagger API 文档
app.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(swaggerSpecs, {
customCss: ".swagger-ui .topbar { display: none }",
customSiteTitle: "qiming-file-server API Documentation",
})
);
// 使用路由配置
app.use(router);
// 404处理中间件必须在所有路由之后
app.use(notFoundHandler);
// 全局错误处理中间件(必须在最后)
app.use(errorHandler);
// 启动服务器
const server = app.listen(config.PORT, async () => {
log(
"default",
"INFO",
`Server is running on port ${config.PORT} (${config.NODE_ENV} mode)`
);
// 项目启动时清理初始化项目文件夹,目的是更新初始化包时,只需要更新zip包
await cleanupInitProjectOnStartup(config);
// 启动 pnpm prune 定时任务
try {
startScheduler();
} catch (error) {
log("default", "ERROR", `pnpm prune scheduled task failed to start: ${error.message}`);
}
});
// 设置服务器超时时间 10 分钟(默认是 120 秒)
// 对于长时间运行的操作(如依赖安装、构建)是必要的
server.timeout = 600000; // 10 分钟
server.keepAliveTimeout = 610000; // 略大于 timeout
server.headersTimeout = 620000; // 略大于 keepAliveTimeout
// 优雅退出处理
const gracefulShutdown = (signal) => {
log("default", "INFO", `Received ${signal} signal, preparing graceful exit...`);
// 停止定时任务
stopScheduler();
// 清理日志缓存管理器(清理定时器)
try {
logCacheManager.destroy();
log("default", "INFO", "Log cache manager cleared");
} catch (error) {
log("default", "ERROR", `Failed to clear log cache manager: ${error.message}`);
}
// 关闭服务器
server.close(() => {
log("default", "INFO", "Server closed");
process.exit(0);
});
// 如果 30 秒后还未退出,强制退出
setTimeout(() => {
log("default", "ERROR", "Force exit (timeout)");
process.exit(1);
}, 30000);
};
// 监听退出信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));