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,231 @@
import express from "express";
import { startDevServer } from "../utils/build/startDevUtils.js";
import { restartDevServer } from "../utils/build/restartDevUtils.js";
import { keepAliveDevServer } from "../utils/build/keepAliveDevUtils.js";
import { stopDevServer } from "../utils/build/stopDevUtils.js";
import { buildProject } from "../utils/build/buildProjectUtils.js";
import { listRunningProcesses } from "../utils/build/processManager.js";
import BuildErrorParser from "../utils/error/buildErrorParser.js";
import { getDevLog } from "../utils/log/getDevLogUtils.js";
import logCacheManager from "../utils/log/logCacheManager.js";
import portPool from "../utils/buildArg/portPool.js";
import {
ValidationError,
BusinessError,
SystemError,
ProcessError,
asyncHandler,
} from "../utils/error/errorHandler.js";
const buildRouter = express.Router();
// 路由配置
const routes = [
{
path: "/start-dev",
method: "get",
handler: asyncHandler(async (req, res) => {
const projectId = req.query.projectId;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
// 设置长超时时间:依赖安装可能需要较长时间
req.setTimeout(600000); // 10分钟
res.setTimeout(600000); // 10分钟
const result = await startDevServer(req, String(projectId));
res.json(result);
}),
},
{
path: "/keep-alive",
method: "get",
handler: asyncHandler(async (req, res) => {
const projectId = req.query.projectId;
const pid = req.query.pid;
const port = req.query.port;
const basePath = req.query.basePath;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (!pid) {
throw new ValidationError("Process ID cannot be empty", { field: "pid" });
}
if (!port) {
throw new ValidationError("Port cannot be empty", { field: "port" });
}
if (!basePath) {
throw new ValidationError("basePath cannot be empty", { field: "basePath" });
}
const result = await keepAliveDevServer(
req,
String(projectId),
pid,
port,
basePath,
);
res.json(result);
}),
},
{
path: "/build",
method: "get",
handler: asyncHandler(async (req, res) => {
const projectId = req.query.projectId;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
// 设置长超时时间:构建可能需要较长时间
req.setTimeout(600000); // 10分钟
res.setTimeout(600000); // 10分钟
const result = await buildProject(req, String(projectId));
res.json(result);
}),
},
{
path: "/stop-dev",
method: "get",
handler: asyncHandler(async (req, res) => {
const projectId = req.query.projectId;
const pid = req.query.pid;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (!pid) {
throw new ValidationError("Process ID cannot be empty", { field: "pid" });
}
const result = await stopDevServer(req, String(projectId), pid, {
strict: true,
});
res.json(result);
}),
},
{
path: "/restart-dev",
method: "get",
handler: asyncHandler(async (req, res) => {
const projectId = req.query.projectId;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
// 设置长超时时间:重启包含依赖安装,可能需要较长时间
req.setTimeout(600000); // 10分钟
res.setTimeout(600000); // 10分钟
const result = await restartDevServer(req, String(projectId));
res.json(result);
}),
},
{
path: "/list-dev",
method: "get",
handler: (req, res) => {
const list = listRunningProcesses();
const result = { success: true, list };
res.json(result);
},
},
{
path: "/parse-build-error",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, errorMessage } = req.body;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (!errorMessage) {
throw new ValidationError("Error message cannot be empty", {
field: "errorMessage",
});
}
const errorParser = new BuildErrorParser();
const userFriendlyMessage = errorParser.parseBuildError(
errorMessage,
String(projectId)
);
res.json({
success: true,
message: userFriendlyMessage,
});
}),
},
{
path: "/get-dev-log",
method: "get",
handler: asyncHandler(async (req, res) => {
const projectId = req.query.projectId;
const startIndex = req.query.startIndex;
const logType = req.query.logType || "temp";
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
// 解析起始行号默认为1
const startLine = startIndex ? parseInt(startIndex, 10) : 1;
if (isNaN(startLine) || startLine < 1) {
throw new ValidationError("Start index must be a positive integer (starting from 1)", {
field: "startIndex",
value: startIndex,
});
}
const result = await getDevLog(String(projectId), startLine, logType);
res.json(result);
}),
},
{
path: "/get-log-cache-stats",
method: "get",
handler: (req, res) => {
const stats = logCacheManager.getStats();
res.json({
success: true,
message: "Get log cache statistics successfully",
stats,
});
},
},
{
path: "/clear-all-log-cache",
method: "get",
handler: (req, res) => {
logCacheManager.clear();
res.json({
success: true,
message: "All log caches have been cleared",
});
},
},
{
path: "/port-pool-status",
method: "get",
handler: (req, res) => {
const status = portPool.getStatus();
res.json({
success: true,
message: "Get port pool status successfully",
...status,
});
},
},
];
// 注册路由
routes.forEach((route) => {
buildRouter[route.method](route.path, route.handler);
});
export default buildRouter;

View File

@@ -0,0 +1,222 @@
import express from "express";
import multer from "multer";
import { asyncHandler, ValidationError } from "../utils/error/errorHandler.js";
import codeService from "../service/codeService.js";
import { log } from "../utils/log/logUtils.js";
import config from "../appConfig/index.js";
import { extractIsolationContext } from "../utils/common/projectPathUtils.js";
const codeRouter = express.Router();
// 配置multer用于内存存储适合小文件
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: config.UPLOAD_SINGLE_FILE_SIZE_BYTES, // 从环境变量读取文件大小限制
},
});
// 路由配置
const routes = [
{
path: "/specified-files-update",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, files } = req.body || {};
const isolationContext = extractIsolationContext(req.body || {});
log(projectId, "INFO", "Partial files update", {
projectId,
codeVersion,
filesCount: files ? files.length : 0,
});
// 解码文件内容(前端使用 encodeURIComponent 编码)
if (files && Array.isArray(files)) {
files.forEach((fileOp) => {
if (!fileOp) return;
// 只要有 contents 就解码
if (typeof fileOp.contents === "string" && fileOp.contents) {
try {
fileOp.contents = decodeURIComponent(fileOp.contents);
} catch (err) {
log(projectId, "WARN", "Decode file content failed", {
fileName: fileOp.path,
error: err.message,
});
// 如果解码失败,保持原样(可能前端没有编码)
}
}
});
}
const result = await codeService.specifiedFilesUpdate(
String(projectId),
String(codeVersion),
files,
req,
isolationContext
);
res.status(200).json(result);
}),
},
{
path: "/all-files-update",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, files, basePath, pid } = req.body || {};
const isolationContext = extractIsolationContext(req.body || {});
log(projectId, "INFO", "Submit files", {
projectId,
codeVersion,
basePath,
pid,
});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (codeVersion === undefined || codeVersion === null) {
throw new ValidationError("codeVersion cannot be empty", {
field: "codeVersion",
});
}
if (!Array.isArray(files)) {
throw new ValidationError("files must be an array", { field: "files" });
}
// 解码文件内容(前端使用 encodeURIComponent 编码)
if (files && Array.isArray(files)) {
files.forEach((file) => {
if (file && typeof file.contents === "string" && file.contents) {
try {
file.contents = decodeURIComponent(file.contents);
} catch (err) {
log(projectId, "WARN", "Decode file content failed", {
fileName: file.name,
error: err.message,
});
// 如果解码失败,保持原样(可能前端没有编码)
}
}
});
}
const result = await codeService.allFilesUpdate(
String(projectId),
String(codeVersion),
files,
req,
isolationContext
);
res.status(200).json(result);
}),
},
{
path: "/upload-single-file",
method: "post",
middleware: upload.single("file"),
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, filePath } = req.body || {};
const isolationContext = extractIsolationContext(req.body || {});
const file = req.file; // 从 multer 中间件获取上传的文件
log(projectId, "INFO", "上传单个文件", {
projectId,
codeVersion,
filePath,
});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (codeVersion === undefined || codeVersion === null) {
throw new ValidationError("codeVersion cannot be empty", {
field: "codeVersion",
});
}
if (!file) {
throw new ValidationError("File cannot be empty", { field: "file" });
}
if (!filePath || typeof filePath !== "string") {
throw new ValidationError("File path cannot be empty", { field: "filePath" });
}
// 记录接收到的文件信息,用于调试
log(projectId, "INFO", "Received file information", {
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
bufferLength: file.buffer ? file.buffer.length : 0,
bufferIsBuffer: Buffer.isBuffer(file.buffer),
});
// 构建文件对象统一使用buffermulter memoryStorage对所有文件类型都提供buffer
const fileObj = {
buffer: file.buffer,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
};
const result = await codeService.uploadSingleFile(
String(projectId),
String(codeVersion),
fileObj,
filePath,
req,
isolationContext
);
res.status(200).json(result);
}),
},
{
path: "/rollback-version",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, rollbackTo } = req.body || {};
const isolationContext = extractIsolationContext(req.body || {});
log(projectId, "INFO", "Rollback version", {
projectId,
codeVersion,
rollbackTo,
});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (codeVersion === undefined || codeVersion === null) {
throw new ValidationError("codeVersion cannot be empty", {
field: "codeVersion",
});
}
if (rollbackTo === undefined || rollbackTo === null) {
throw new ValidationError("rollbackTo cannot be empty", {
field: "rollbackTo",
});
}
const result = await codeService.rollbackVersion(
String(projectId),
String(codeVersion),
String(rollbackTo),
req,
isolationContext
);
res.status(200).json(result);
}),
},
];
// 注册路由
routes.forEach((route) => {
if (route.middleware) {
// 如果有中间件,先注册中间件再注册处理器
codeRouter[route.method](route.path, route.middleware, route.handler);
} else {
// 没有中间件,直接注册处理器
codeRouter[route.method](route.path, route.handler);
}
});
export default codeRouter;

View File

@@ -0,0 +1,414 @@
import express from "express";
import multer from "multer";
import fs from "fs";
import path from "path";
import { ValidationError, asyncHandler } from "../utils/error/errorHandler.js";
import { log } from "../utils/log/logUtils.js";
import config from "../appConfig/index.js";
import { createWorkspace, pushSkillsToWorkspace, } from "../utils/computer/computerUtils.js";
import {
getFileList,
updateFiles,
uploadFile,
uploadFiles,
downloadAllFiles,
} from "../utils/computer/computerFileUtils.js";
const computerRouter = express.Router();
// 使用磁盘存储,便于后续解压 zip
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
try {
// 将临时上传目录放到 COMPUTER_WORKSPACE_DIR/<userId>/<cId>/.tmp 下
const baseDir = config.COMPUTER_WORKSPACE_DIR;
if (!baseDir) {
return cb(
new Error("COMPUTER_WORKSPACE_DIR is not configured, cannot determine upload temporary directory")
);
}
const userId = req.body?.userId || "unknown";
const cId = req.body?.cId || "unknown";
const tmpUploadDir = path.join(
baseDir,
String(userId),
String(cId),
".tmp"
);
if (!fs.existsSync(tmpUploadDir)) {
fs.mkdirSync(tmpUploadDir, { recursive: true });
}
cb(null, tmpUploadDir);
} catch (err) {
cb(err);
}
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname) || ".zip";
const baseName = path.basename(file.originalname, ext);
const uniqueSuffix = `${Date.now()}_${Math.round(Math.random() * 1e6)}`;
cb(null, `${baseName}_${uniqueSuffix}${ext}`);
},
}),
limits: {
fileSize: config.UPLOAD_MAX_FILE_SIZE_BYTES,
},
});
// Multer 错误处理中间件(复用通用模式)
function multerErrorHandler(err, req, res, next) {
if (err && err.name === "MulterError") {
return next(err);
}
if (err && (err.name === "ValidationError" || err instanceof ValidationError)) {
return next(err);
}
next(err);
}
const routes = [
{
path: "/create-workspace",
method: "post",
middleware: upload.single("file"),
handler: asyncHandler(async (req, res) => {
const { userId, cId } = req.body || {};
const file = req.file || null;
const logId = `computer:${userId}:${cId}`;
log(logId, "INFO", "Create workspace request", {
userId,
cId,
hasFile: !!file,
fileName: file?.originalname,
fileSize: file?.size,
});
// 兼容老逻辑:仅处理上传 file不处理 skillUrls
const result = await createWorkspace(userId, cId, file);
res.status(200).json({
success: true,
...result,
});
}),
},
{
path: "/create-workspace-v2",
method: "post",
middleware: upload.single("file"),
handler: asyncHandler(async (req, res) => {
const { userId, cId, skillUrls } = req.body || {};
const file = req.file || null;
const logId = `computer:${userId}:${cId}`;
// multipart/form-data 中数组可能被序列化成 JSON 字符串
let normalizedSkillUrls = skillUrls;
if (typeof skillUrls === "string") {
try {
const parsed = JSON.parse(skillUrls);
normalizedSkillUrls = Array.isArray(parsed) ? parsed : [skillUrls];
} catch {
normalizedSkillUrls = [skillUrls];
}
}
log(logId, "INFO", "Create workspace v2 request", {
userId,
cId,
hasFile: !!file,
fileName: file?.originalname,
fileSize: file?.size,
skillUrlsCount: Array.isArray(normalizedSkillUrls) ? normalizedSkillUrls.length : 0,
});
const result = await createWorkspace(userId, cId, file, normalizedSkillUrls);
res.status(200).json({
success: true,
...result,
});
}),
},
{
path: "/push-skills-to-workspace",
method: "post",
middleware: upload.single("file"),
handler: asyncHandler(async (req, res) => {
const { userId, cId } = req.body || {};
const file = req.file || null;
const logId = `computer:${userId}:${cId}`;
log(logId, "INFO", "推送技能到工作空间请求", {
userId,
cId,
hasFile: !!file,
fileName: file?.originalname,
fileSize: file?.size,
});
// 兼容老逻辑:仅处理上传 file不处理 skillUrls
const result = await pushSkillsToWorkspace(userId, cId, file);
res.status(200).json({
success: true,
...result,
});
}),
},
{
path: "/push-skills-to-workspace-v2",
method: "post",
middleware: upload.single("file"),
handler: asyncHandler(async (req, res) => {
const { userId, cId, skillUrls } = req.body || {};
const file = req.file || null;
const logId = `computer:${userId}:${cId}`;
// multipart/form-data 中数组可能被序列化成 JSON 字符串
let normalizedSkillUrls = skillUrls;
if (typeof skillUrls === "string") {
try {
const parsed = JSON.parse(skillUrls);
normalizedSkillUrls = Array.isArray(parsed) ? parsed : [skillUrls];
} catch {
normalizedSkillUrls = [skillUrls];
}
}
log(logId, "INFO", "推送技能到工作空间请求(v2)", {
userId,
cId,
hasFile: !!file,
fileName: file?.originalname,
fileSize: file?.size,
skillUrlsCount: Array.isArray(normalizedSkillUrls) ? normalizedSkillUrls.length : 0,
});
const result = await pushSkillsToWorkspace(userId, cId, file, normalizedSkillUrls);
res.status(200).json({
success: true,
...result,
});
}),
},
{
path: "/get-file-list",
method: "get",
handler: asyncHandler(async (req, res) => {
const { userId, cId, proxyPath } = req.query;
const result = await getFileList(userId, cId, proxyPath);
res.status(200).json({ success: true, ...result });
}),
},
{
path: "/files-update",
method: "post",
handler: asyncHandler(async (req, res) => {
const { userId, cId, files } = req.body || {};
const logId = `computer:${userId}:${cId}`;
log(logId, "INFO", "Files update", {
userId,
cId,
filesCount: files ? files.length : 0,
});
// 解码文件内容(前端使用 encodeURIComponent 编码)
if (files && Array.isArray(files)) {
files.forEach((fileOp) => {
if (!fileOp) return;
// 只要有 contents 就解码
if (typeof fileOp.contents === "string" && fileOp.contents) {
try {
fileOp.contents = decodeURIComponent(fileOp.contents);
} catch (err) {
log(logId, "WARN", "Decode file content failed", {
fileName: fileOp.name,
error: err.message,
});
// 如果解码失败,保持原样(可能前端没有编码)
}
}
});
}
const result = await updateFiles(userId, cId, files);
res.status(200).json(result);
}),
},
{
path: "/upload-file",
method: "post",
middleware: upload.single("file"),
handler: asyncHandler(async (req, res) => {
const { userId, cId, filePath } = req.body || {};
const file = req.file; // 从 multer 中间件获取上传的文件
const logId = `computer:${userId}:${cId}`;
log(logId, "INFO", "Upload single file", {
userId,
cId,
filePath,
});
// 从磁盘读取文件内容diskStorage 模式下 file.buffer 不存在)
const fileBuffer = await fs.promises.readFile(file.path);
// 构建文件对象,支持文本和二进制文件
const fileObj = {
buffer: fileBuffer,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
};
try {
const result = await uploadFile(userId, cId, fileObj, filePath);
res.status(200).json(result);
} finally {
// 清理临时文件
if (fs.existsSync(file.path)) {
await fs.promises.unlink(file.path);
}
}
}),
},
{
path: "/upload-files",
method: "post",
middleware: upload.array("files"),
handler: asyncHandler(async (req, res) => {
const { userId, cId, filePaths } = req.body || {};
const files = req.files || []; // 从 multer 中间件获取上传的文件数组
const logId = `computer:${userId}:${cId}`;
// 标准化 filePaths 为数组格式,兼容单文件上传时可能传入字符串的情况
const normalizedFilePaths = Array.isArray(filePaths)
? filePaths
: typeof filePaths === "string"
? [filePaths]
: filePaths;
log(logId, "INFO", "Batch upload files request", {
userId,
cId,
filesCount: files.length,
filePathsCount: Array.isArray(normalizedFilePaths) ? normalizedFilePaths.length : 0,
});
// 从磁盘读取所有文件内容diskStorage 模式下 file.buffer 不存在)
const fileObjects = [];
const tempFilePaths = [];
try {
for (const file of files) {
tempFilePaths.push(file.path);
const fileBuffer = await fs.promises.readFile(file.path);
fileObjects.push({
buffer: fileBuffer,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
});
}
// 调用批量上传工具函数
const result = await uploadFiles(userId, cId, fileObjects, normalizedFilePaths);
res.status(200).json(result);
} finally {
// 清理所有临时文件
for (const tempPath of tempFilePaths) {
if (fs.existsSync(tempPath)) {
try {
await fs.promises.unlink(tempPath);
} catch (error) {
log(logId, "WARN", "Clean temporary file failed", {
tempPath,
error: error.message,
});
}
}
}
}
}),
},
{
path: "/download-all-files",
method: "get",
handler: asyncHandler(async (req, res) => {
const { userId, cId } = req.query || {};
const logId = `computer:${userId}:${cId}`;
log(logId, "INFO", "Download all files request", {
userId,
cId,
});
const { archive, zipFileName } = await downloadAllFiles(
userId,
cId
);
res.setHeader("Content-Type", "application/zip");
// 兼容中文文件名
const encodedName = encodeURIComponent(zipFileName);
res.setHeader(
"Content-Disposition",
`attachment; filename="${encodedName}"; filename*=UTF-8''${encodedName}`
);
archive.on("error", (err) => {
// 让全局错误处理中间件接管
res.destroy(err);
});
archive.pipe(res);
archive.finalize();
}),
},
];
routes.forEach((route) => {
if (route.middleware) {
// 如果有中间件,先注册中间件再注册处理器
computerRouter[route.method](route.path, route.middleware, route.handler);
} else if (route.customHandler) {
// 对于有自定义处理器的路由
const middlewares = [];
// 先添加multer中间件
if (route.handler) {
middlewares.push(route.handler);
}
// 再添加文件名解码中间件(如果有)
if (route.decodeMiddleware) {
middlewares.push(route.decodeMiddleware);
}
// 添加错误处理中间件
middlewares.push(multerErrorHandler);
// 最后添加自定义处理器
middlewares.push(route.customHandler);
// 注册所有中间件
computerRouter[route.method](route.path, ...middlewares);
} else {
// 普通路由直接注册处理器
computerRouter[route.method](route.path, route.handler);
}
});
export default computerRouter;

View File

@@ -0,0 +1,552 @@
import express from "express";
import multer from "multer";
import path from "path";
import fs from "fs";
import iconv from "iconv-lite";
import projectService from "../service/projectService.js";
import {
getProjectContent,
getProjectContentByVersion,
} from "../utils/project/getContentUtils.js";
import { uploadAttachmentFile } from "../utils/project/uploadAttachmentFileUtils.js";
import { copyProject } from "../utils/project/copyProjectUtils.js";
import config from "../appConfig/index.js";
import { log } from "../utils/log/logUtils.js";
import {
extractIsolationContext,
resolveProjectPath,
} from "../utils/common/projectPathUtils.js";
import {
ValidationError,
SystemError,
asyncHandler,
} from "../utils/error/errorHandler.js";
const projectRouter = express.Router();
// 配置multer用于文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
// 先保存到临时目录,后续在路由处理器中移动到项目目录
const uploadDir = path.join(config.UPLOAD_PROJECT_DIR, "temp");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e6);
cb(
null,
file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
);
},
});
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
const ext = path.extname(file.originalname).toLowerCase();
if (config.UPLOAD_ALLOWED_EXTENSIONS.includes(ext)) {
cb(null, true);
} else {
// 将文件类型错误转换为 ValidationError
cb(
new ValidationError("File type not allowed", {
fileExtension: ext,
allowedExtensions: config.UPLOAD_ALLOWED_EXTENSIONS,
}),
false
);
}
},
limits: {
fileSize: config.UPLOAD_MAX_FILE_SIZE_BYTES,
},
});
// 为上传附件文件创建独立的multer配置支持更多文件类型
const attachmentStorage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(config.UPLOAD_PROJECT_DIR, "temp");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e6);
cb(
null,
file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
);
},
});
// 允许的附件文件扩展名常见文档、图片、PDF等
const ATTACHMENT_ALLOWED_EXTENSIONS = [
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".txt",
".md",
".csv",
".json",
".xml",
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".svg",
".ico",
".webp",
".avif",
".zip",
".rar",
".7z",
".tar",
".gz",
".mp4",
".avi",
".mov",
".wmv",
".flv",
".mp3",
".wav",
".ogg",
".m4a",
];
const uploadAttachment = multer({
storage: attachmentStorage,
fileFilter: function (req, file, cb) {
const ext = path.extname(file.originalname).toLowerCase();
if (ATTACHMENT_ALLOWED_EXTENSIONS.includes(ext)) {
cb(null, true);
} else {
// 将文件类型错误转换为 ValidationError
cb(
new ValidationError("附件文件类型不被允许", {
fileExtension: ext,
allowedExtensions: ATTACHMENT_ALLOWED_EXTENSIONS,
}),
false
);
}
},
limits: {
fileSize: config.UPLOAD_MAX_FILE_SIZE_BYTES,
},
});
// 文件名解码中间件,用于正确处理中文文件名
function decodeFileNameMiddleware(req, res, next) {
if (req.file && req.file.originalname) {
try {
// 保存原始文件名用于日志
const beforeDecode = req.file.originalname;
// 尝试将文件名从 latin1 解码为 UTF-8
// 这是一个常见的编码问题,当文件名包含中文字符时
const decodedName = iconv.decode(
Buffer.from(req.file.originalname, "latin1"),
"utf8"
);
req.file.originalname = decodedName;
log("system", "INFO", "File name decoded successfully", {
before: beforeDecode,
after: decodedName,
});
} catch (err) {
// 如果解码失败,记录日志但继续处理
log("system", "WARN", "File name decoded failed", {
originalName: req.file.originalname,
error: err.message,
});
}
}
next();
}
// 路由配置
const routes = [
{
path: "/push-skills-to-workspace",
method: "post",
handler: upload.single("file"),
customHandler: asyncHandler(async (req, res) => {
const { projectId, skillUrls } = req.body || {};
const file = req.file || null;
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
const isolationContext = extractIsolationContext(req.body || {});
const result = await projectService.pushSkillsToWorkspace(
String(projectId),
file,
skillUrls,
isolationContext
);
res.status(200).json({ success: true, ...result });
}),
},
{
path: "/create-project",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, templateType } = req.body;
const isolationContext = extractIsolationContext(req.body || {});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
const result = await projectService.createProject(
String(projectId),
templateType,
isolationContext
);
res.status(200).json(result);
}),
},
{
path: "/upload-project",
method: "post",
handler: upload.single("file"),
customHandler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, pid, basePath } = req.body;
const isolationContext = extractIsolationContext(req.body || {});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (!codeVersion) {
throw new ValidationError("Code version cannot be empty", { field: "codeVersion" });
}
if (!req.file) {
throw new ValidationError("Please upload a zip file", { field: "zipFile" });
}
// 处理文件上传(移动到项目目录)
const uploadResult = await projectService.handleFileUpload(
String(projectId),
codeVersion,
req.file
);
// 更新req.file.path为新的路径
req.file.path = uploadResult.filePath;
try {
const result = await projectService.uploadProject(
String(projectId),
req.file.path,
req,
codeVersion,
pid,
basePath,
isolationContext
);
res.status(200).json(result);
} catch (err) {
// 失败时清理项目目录uploadProject内部已经处理这里作为额外保障
try {
await projectService.cleanupProjectDirectory(
String(projectId),
isolationContext
);
} catch (cleanupErr) {
log(projectId, "ERROR", "Route layer cleanup project directory failed", {
projectId,
error: cleanupErr.message,
});
}
// 清理上传的文件
if (req.file && fs.existsSync(req.file.path)) {
try {
fs.unlinkSync(req.file.path);
} catch (cleanupErr) {
log(projectId, "ERROR", "Clean upload file failed", {
projectId,
error: cleanupErr.message,
});
}
}
throw err; // 重新抛出错误,让全局错误处理器处理
}
}),
},
{
path: "/get-project-content",
method: "get",
handler: asyncHandler(async (req, res) => {
const { projectId, command, proxyPath } = req.query;
const isolationContext = extractIsolationContext(req.query || {});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
// 构建项目路径
const projectPath = resolveProjectPath(projectId, isolationContext);
// 检查项目目录是否存在
if (!fs.existsSync(projectPath)) {
throw new ValidationError("Project does not exist", { field: "projectId" });
}
// 获取项目内容
try {
const result = await getProjectContent(
projectPath,
command,
proxyPath
);
res.status(200).json({ success: true, ...result });
} catch (err) {
const message = err?.message || "Query failed";
res.status(500).json({ success: false, message });
}
}),
},
{
path: "/get-project-content-by-version",
method: "get",
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, command, proxyPath } = req.query;
const isolationContext = extractIsolationContext(req.query || {});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (!codeVersion) {
throw new ValidationError("Code version cannot be empty", { field: "codeVersion" });
}
try {
const result = await getProjectContentByVersion(
String(projectId),
codeVersion,
command,
proxyPath,
isolationContext
);
res.status(200).json({ success: true, ...result });
} catch (err) {
const message = err?.message || "Query failed";
res.status(500).json({ success: false, message });
}
}),
},
{
path: "/backup-current-version",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion } = req.body;
const isolationContext = extractIsolationContext(req.body || {});
const result = await projectService.backupCurrentVersion(
String(projectId),
codeVersion,
isolationContext
);
res.status(200).json({ success: true, ...result });
}),
},
{
path: "/export-project",
method: "post",
handler: asyncHandler(async (req, res) => {
const { projectId, codeVersion, exportType, config } = req.body;
const isolationContext = extractIsolationContext(req.body || {});
const result = await projectService.exportProject(
String(projectId),
codeVersion,
exportType,
config,
isolationContext
);
// 检查zip文件是否存在
if (!fs.existsSync(result.zipPath)) {
throw new SystemError("Exported zip file does not exist", {
zipPath: result.zipPath,
});
}
// 设置响应头,指定文件下载
const fileName = path.basename(result.zipPath);
res.setHeader("Content-Type", "application/zip");
res.setHeader(
"Content-Disposition",
`attachment; filename="${fileName}"`
);
res.sendFile(result.zipPath, (err) => {
if (err) {
log(projectId, "ERROR", "Send zip file failed", {
projectId,
error: err.message,
});
if (!res.headersSent) {
res.status(500).json({ success: false, message: "File send failed" });
}
} else {
log(projectId, "INFO", "Zip file send successfully", {
projectId,
zipPath: result.zipPath,
});
}
});
}),
},
{
path: "/delete-project",
method: "get",
handler: asyncHandler(async (req, res) => {
const { projectId, pid } = req.query;
const isolationContext = extractIsolationContext(req.query || {});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
const result = await projectService.deleteProject(
String(projectId),
pid,
req,
isolationContext
);
res.status(200).json(result);
}),
},
{
path: "/upload-attachment-file",
method: "post",
handler: uploadAttachment.single("file"),
decodeMiddleware: decodeFileNameMiddleware,
customHandler: asyncHandler(async (req, res) => {
const { projectId, fileName } = req.body;
const isolationContext = extractIsolationContext(req.body || {});
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
if (!req.file) {
throw new ValidationError("Please upload a file", { field: "file" });
}
try {
const result = await uploadAttachmentFile(
String(projectId),
req.file,
fileName,
isolationContext
);
res.status(200).json({ success: true, ...result });
} catch (err) {
// 失败时清理上传的文件
if (req.file && fs.existsSync(req.file.path)) {
try {
fs.unlinkSync(req.file.path);
} catch (cleanupErr) {
log(projectId, "ERROR", "Clean upload file failed", {
projectId,
error: cleanupErr.message,
});
}
}
throw err; // 重新抛出错误,让全局错误处理器处理
}
}),
},
{
path: "/copy-project",
method: "post",
handler: asyncHandler(async (req, res) => {
const { sourceProjectId, targetProjectId } = req.body;
const sourceIsolationContext = extractIsolationContext({
tenantId: req.body?.sourceTenantId || req.body?.tenantId,
spaceId: req.body?.sourceSpaceId || req.body?.spaceId,
isolationType: req.body?.sourceIsolationType || req.body?.isolationType,
});
const targetIsolationContext = extractIsolationContext({
tenantId: req.body?.targetTenantId || req.body?.tenantId,
spaceId: req.body?.targetSpaceId || req.body?.spaceId,
isolationType: req.body?.targetIsolationType || req.body?.isolationType,
});
if (!sourceProjectId) {
throw new ValidationError("Source project ID cannot be empty", {
field: "sourceProjectId",
});
}
if (!targetProjectId) {
throw new ValidationError("Target project ID cannot be empty", {
field: "targetProjectId",
});
}
const result = await copyProject(
String(sourceProjectId),
String(targetProjectId),
{
sourceIsolationContext,
targetIsolationContext,
}
);
res.status(200).json(result);
}),
},
];
// Multer错误处理中间件
function multerErrorHandler(err, req, res, next) {
if (err instanceof multer.MulterError) {
// Multer错误会被全局错误处理器捕获并处理
return next(err);
}
// 如果是 ValidationError直接传递给错误处理器
if (err.name === "ValidationError" || err instanceof ValidationError) {
return next(err);
}
next(err);
}
// 注册路由
routes.forEach((route) => {
if (route.customHandler) {
// 对于有自定义处理器的路由
const middlewares = [];
// 先添加multer中间件
if (route.handler) {
middlewares.push(route.handler);
}
// 再添加文件名解码中间件(如果有)
if (route.decodeMiddleware) {
middlewares.push(route.decodeMiddleware);
}
// 添加错误处理中间件
middlewares.push(multerErrorHandler);
// 最后添加自定义处理器
middlewares.push(route.customHandler);
// 注册所有中间件
projectRouter[route.method](route.path, ...middlewares);
} else {
// 普通路由直接注册处理器
projectRouter[route.method](route.path, route.handler);
}
});
export default projectRouter;

View File

@@ -0,0 +1,56 @@
import express from "express";
import buildRouter from "./buildRoutes.js";
import projectRouter from "./projectRoutes.js";
import codeRouter from "./codeRoutes.js";
import computerRouter from "./computerRoutes.js";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 读取 package.json 获取版本号(使用绝对路径确保跨场景兼容)
let version = "1.0.0";
try {
const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
version = packageJson.version || "1.0.0";
} catch (err) {
// 使用默认值
}
const router = express.Router();
router.get("/", (req, res) => {
res.send("Hello");
});
router.get("/health", (req, res) => {
const uptimeSeconds = Math.floor(process.uptime());
const memoryUsage = process.memoryUsage();
const memory = {
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024 * 100) / 100,
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024 * 100) / 100,
rss: Math.round(memoryUsage.rss / 1024 / 1024 * 100) / 100,
external: Math.round(memoryUsage.external / 1024 / 1024 * 100) / 100,
};
const healthData = {
status: "ok",
timestamp: Date.now(),
uptime: uptimeSeconds,
version: version,
platform: process.platform,
nodeVersion: process.version,
pid: process.pid,
memory: memory,
env: process.env.NODE_ENV || "unknown",
};
res.json(healthData);
});
router.use("/api/build", buildRouter);
router.use("/api/project", projectRouter);
router.use("/api/project", codeRouter);
router.use("/api/computer", computerRouter);
export default router;