import * as http from "http"; import * as https from "https"; import * as fs from "fs"; import * as path from "path"; import * as zlib from "zlib"; import sqlite3 from "sqlite3"; // 配置文件路径 const CONFIG_FILE = path.join(__dirname, "config.json"); const API_LIST_FILE = path.join(__dirname, "mock", "api-list.json"); const MOCK_DIR = path.join(__dirname, "mock"); const DATA_DIR = path.join(__dirname, "data"); const DB_FILE = path.join(DATA_DIR, "mock-mappings.sqlite3"); const LEGACY_DB_FILE = path.join(MOCK_DIR, "mock-mappings.sqlite3"); // 定义类型 interface RouteConfig { [route: string]: string; } interface RouteStatusConfig { [route: string]: number; } interface AppConfig { /** 为 false 时所有请求走代理,不命中 mock 路由;缺省为 true */ mockEnabled?: boolean; cacheConfig: boolean; reloadOnChange: boolean; defaultContentType: string; proxyPort: number; targetHost: string; /** 代理转发的目标端口;缺省 HTTPS 为 443,HTTP 为 80 */ targetPort?: number; /** 为 false 时用 HTTP 连接上游;缺省 true(HTTPS) */ targetHttps?: boolean; } interface ConfigFile { routes: RouteConfig; routeStatuses?: RouteStatusConfig; config: AppConfig; } interface ApiListItem { name: string; route: string; } interface MockFileItem { filePath: string; alias: string; content: string; } interface RouteRow { route_key: string; file_path: string; status_code: number; } interface MockFileRow { file_path: string; alias: string; } // 存储当前的路由配置 let MOCK_ROUTES: RouteConfig = {}; let RAW_ROUTES: RouteConfig = {}; let MOCK_ROUTE_STATUSES: RouteStatusConfig = {}; let RAW_ROUTE_STATUSES: RouteStatusConfig = {}; let CONFIG: AppConfig = { cacheConfig: true, reloadOnChange: true, defaultContentType: "application/json", proxyPort: 443, targetHost: "localhost", targetPort: 443, }; let DB: sqlite3.Database; // 过滤掉以 # 开头的路由(视为注释,不参与 mock) function filterActiveRoutes(routes: RouteConfig): RouteConfig { const filtered: RouteConfig = {}; for (const [route, filePath] of Object.entries(routes)) { if (route.startsWith("#")) continue; filtered[route] = filePath; } return filtered; } function filterActiveRouteStatuses(statuses: RouteStatusConfig): RouteStatusConfig { const filtered: RouteStatusConfig = {}; for (const [route, statusCode] of Object.entries(statuses)) { if (route.startsWith("#")) continue; filtered[route] = statusCode; } return filtered; } function normalizeStatusCode(input: unknown, fallback = 200): number { const numeric = typeof input === "number" ? input : Number.parseInt(String(input ?? ""), 10); if (Number.isInteger(numeric) && numeric >= 100 && numeric <= 599) { return numeric; } return fallback; } function isTargetHttps(): boolean { return CONFIG.targetHttps !== false; } function getTargetPort(): number { if (CONFIG.targetPort != null) return CONFIG.targetPort; return isTargetHttps() ? 443 : 80; } function upstreamRequest( options: http.RequestOptions, callback: (proxyRes: http.IncomingMessage) => void, ): http.ClientRequest { return isTargetHttps() ? https.request(options, callback) : http.request(options, callback); } function dbRun(sql: string, params: unknown[] = []): Promise { return new Promise((resolve, reject) => { DB.run(sql, params, (error) => { if (error) { reject(error); return; } resolve(); }); }); } function dbAll(sql: string, params: unknown[] = []): Promise { return new Promise((resolve, reject) => { DB.all(sql, params, (error, rows) => { if (error) { reject(error); return; } resolve((rows as T[]) || []); }); }); } function openDatabase(): Promise { return new Promise((resolve, reject) => { DB = new sqlite3.Database(DB_FILE, (error) => { if (error) { reject(error); return; } resolve(); }); }); } async function initDatabase(): Promise { fs.mkdirSync(MOCK_DIR, { recursive: true }); fs.mkdirSync(DATA_DIR, { recursive: true }); if (!fs.existsSync(DB_FILE) && fs.existsSync(LEGACY_DB_FILE)) { fs.copyFileSync(LEGACY_DB_FILE, DB_FILE); } await openDatabase(); await dbRun(` CREATE TABLE IF NOT EXISTS route_mappings ( route_key TEXT PRIMARY KEY, file_path TEXT NOT NULL, status_code INTEGER NOT NULL DEFAULT 200 ) `); await dbRun( "ALTER TABLE route_mappings ADD COLUMN status_code INTEGER NOT NULL DEFAULT 200", ).catch(() => { // ignore when column already exists }); await dbRun(` CREATE TABLE IF NOT EXISTS api_list ( route TEXT PRIMARY KEY, name TEXT NOT NULL ) `); await dbRun(` CREATE TABLE IF NOT EXISTS mock_files ( file_path TEXT PRIMARY KEY, alias TEXT NOT NULL DEFAULT '' ) `); await dbRun("ALTER TABLE mock_files ADD COLUMN alias TEXT NOT NULL DEFAULT ''").catch( () => { // ignore when column already exists }, ); // 清理不该出现在 mock 列表中的系统文件 await dbRun( "DELETE FROM mock_files WHERE file_path = ? OR file_path LIKE ?", ["mock/api-list.json", "%.sqlite3"], ); } async function saveRoutesToDb( routes: RouteConfig, routeStatuses: RouteStatusConfig = {}, ): Promise { await dbRun("DELETE FROM route_mappings"); for (const [routeKey, filePath] of Object.entries(routes)) { const statusCode = normalizeStatusCode(routeStatuses[routeKey], 200); await dbRun( "INSERT INTO route_mappings(route_key, file_path, status_code) VALUES(?, ?, ?)", [routeKey, filePath, statusCode], ); await dbRun("INSERT OR IGNORE INTO mock_files(file_path) VALUES(?)", [ filePath, ]); } } async function loadRoutesFromDb(): Promise<{ routes: RouteConfig; routeStatuses: RouteStatusConfig; }> { const rows = await dbAll( "SELECT route_key, file_path, status_code FROM route_mappings ORDER BY route_key ASC", ); const routes: RouteConfig = {}; const routeStatuses: RouteStatusConfig = {}; for (const row of rows) { routes[row.route_key] = row.file_path; routeStatuses[row.route_key] = normalizeStatusCode(row.status_code, 200); } return { routes, routeStatuses }; } async function upsertApiListToDb(list: ApiListItem[]): Promise { await dbRun("DELETE FROM api_list"); for (const item of list) { await dbRun("INSERT INTO api_list(route, name) VALUES(?, ?)", [ item.route, item.name, ]); } } async function loadApiListFromDb(): Promise { const rows = await dbAll<{ route: string; name: string }>( "SELECT route, name FROM api_list ORDER BY route ASC", ); return rows.map((row) => ({ route: row.route, name: row.name })); } async function upsertMockFilePathToDb(filePath: string, alias?: string): Promise { if (typeof alias === "string") { await dbRun( "INSERT INTO mock_files(file_path, alias) VALUES(?, ?) ON CONFLICT(file_path) DO UPDATE SET alias = excluded.alias", [filePath, alias], ); return; } await dbRun("INSERT OR IGNORE INTO mock_files(file_path, alias) VALUES(?, '')", [ filePath, ]); } async function removeMockFilePathFromDb(filePath: string): Promise { await dbRun("DELETE FROM mock_files WHERE file_path = ?", [filePath]); } async function loadMockFilePathsFromDb(): Promise { const rows = await dbAll( "SELECT file_path, alias FROM mock_files ORDER BY file_path ASC", ); return rows.map((row) => ({ file_path: row.file_path, alias: String(row.alias || ""), })); } // 加载配置文件 async function loadConfig() { try { if (!fs.existsSync(CONFIG_FILE)) { console.warn(`[CONFIG] 配置文件不存在: ${CONFIG_FILE}`); console.warn(`[CONFIG] 正在创建默认配置文件...`); // 创建默认配置 const defaultConfig: ConfigFile = { routes: { "/api2/test": "mock/test.txt", }, config: { cacheConfig: true, reloadOnChange: true, defaultContentType: "application/json", proxyPort: 443, targetHost: "localhost", targetPort: 443, }, }; // 确保mock目录存在 const mockDir = path.join(__dirname, "mock"); if (!fs.existsSync(mockDir)) { fs.mkdirSync(mockDir, { recursive: true }); } // 保存配置文件 fs.writeFileSync( CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), "utf-8", ); console.log(`[CONFIG] 已创建默认配置文件: ${CONFIG_FILE}`); // 加载配置,路由最终以 sqlite 为准 const configData: ConfigFile = JSON.parse( fs.readFileSync(CONFIG_FILE, "utf-8"), ); CONFIG = configData.config || CONFIG; await saveRoutesToDb(configData.routes || {}, configData.routeStatuses || {}); } else { const configData: ConfigFile = JSON.parse( fs.readFileSync(CONFIG_FILE, "utf-8"), ); CONFIG = configData.config || CONFIG; const dbMappings = await loadRoutesFromDb(); if (Object.keys(dbMappings.routes).length === 0 && configData.routes) { await saveRoutesToDb(configData.routes, configData.routeStatuses || {}); } } const dbMappings = await loadRoutesFromDb(); RAW_ROUTES = dbMappings.routes; RAW_ROUTE_STATUSES = dbMappings.routeStatuses; MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES); MOCK_ROUTE_STATUSES = filterActiveRouteStatuses(RAW_ROUTE_STATUSES); console.log( `[CONFIG] 配置已加载(sqlite 路由 ${Object.keys(MOCK_ROUTES).length} 条)${CONFIG.mockEnabled !== false ? "" : "(mock 已关闭,全部走代理)"}`, ); // 验证mock文件是否存在 validateMockFiles(); } catch (error) { console.error(`[CONFIG] 加载配置文件失败:`, error); // 使用默认配置 MOCK_ROUTES = { "/api2/user/list": "mock/user_list.txt", }; RAW_ROUTES = { ...MOCK_ROUTES }; RAW_ROUTE_STATUSES = {}; MOCK_ROUTE_STATUSES = {}; CONFIG = { cacheConfig: true, reloadOnChange: true, defaultContentType: "application/json", proxyPort: 9443, targetHost: "devrmtapp.resmart.cn", targetPort: 443, }; } } function buildCurrentConfigFile(): ConfigFile { return { routes: { ...RAW_ROUTES }, routeStatuses: { ...RAW_ROUTE_STATUSES }, config: { ...CONFIG }, }; } async function saveConfigFile(nextConfig: ConfigFile): Promise { // routes 持久化到 sqlite,config 仍使用 config.json await saveRoutesToDb(nextConfig.routes || {}, nextConfig.routeStatuses || {}); fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), "utf-8"); const dbMappings = await loadRoutesFromDb(); RAW_ROUTES = dbMappings.routes; RAW_ROUTE_STATUSES = dbMappings.routeStatuses; MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES); MOCK_ROUTE_STATUSES = filterActiveRouteStatuses(RAW_ROUTE_STATUSES); CONFIG = nextConfig.config || CONFIG; } function loadApiListFromJson(): ApiListItem[] { if (!fs.existsSync(API_LIST_FILE)) { return []; } try { const text = fs.readFileSync(API_LIST_FILE, "utf-8").trim(); if (!text) { return []; } const parsed = JSON.parse(text); if (!Array.isArray(parsed)) { return []; } const seenRoutes = new Set(); const list: ApiListItem[] = []; for (const item of parsed) { const route = String(item?.route || "").trim(); const name = String(item?.name || "").trim(); if (!route || !route.startsWith("/") || !name || seenRoutes.has(route)) { continue; } seenRoutes.add(route); list.push({ name, route }); } return list; } catch (error) { console.warn("[ADMIN] 读取 api-list.json 失败:", error); return []; } } async function loadApiList(): Promise { const dbList = await loadApiListFromDb(); if (dbList.length > 0) { return dbList; } const jsonList = loadApiListFromJson(); if (jsonList.length > 0) { await upsertApiListToDb(jsonList); } return jsonList; } function normalizeMockFilePath(filePath: string): string { const trimmed = filePath.trim().replace(/\\/g, "/"); if (!trimmed) { throw new Error("filePath is required"); } const relative = trimmed.startsWith("mock/") ? trimmed : `mock/${trimmed}`; if (path.isAbsolute(relative)) { throw new Error("filePath must be a relative path"); } return relative; } function resolveMockFullPath(relativePath: string): string { const normalized = normalizeMockFilePath(relativePath); const fullPath = path.resolve(__dirname, normalized); const mockRoot = path.resolve(MOCK_DIR); if (!fullPath.startsWith(mockRoot)) { throw new Error("filePath is invalid"); } if (path.basename(fullPath) === "api-list.json") { throw new Error("api-list.json is read-only in this panel"); } return fullPath; } function walkMockFiles(dir: string, baseDir: string, result: string[]): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkMockFiles(fullPath, baseDir, result); continue; } if (!entry.isFile()) continue; if (entry.name === "api-list.json") continue; if (entry.name.endsWith(".sqlite3")) continue; const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/"); result.push(`mock/${relative}`); } } async function loadMockFiles(): Promise { let files = await loadMockFilePathsFromDb(); files = files.filter( (item) => item.file_path !== "mock/api-list.json" && !item.file_path.endsWith(".sqlite3"), ); if (files.length === 0 && fs.existsSync(MOCK_DIR)) { const fsFiles: string[] = []; walkMockFiles(MOCK_DIR, MOCK_DIR, fsFiles); fsFiles.sort((a, b) => a.localeCompare(b)); for (const filePath of fsFiles) { await upsertMockFilePathToDb(filePath); } files = fsFiles.map((filePath) => ({ file_path: filePath, alias: "" })); } const result: MockFileItem[] = []; for (const item of files) { const filePath = item.file_path; const fullPath = path.join(__dirname, filePath); if (!fs.existsSync(fullPath)) continue; result.push({ filePath, alias: String(item.alias || ""), content: fs.readFileSync(fullPath, "utf-8"), }); } return result; } async function initMockFilesFromFsIfNeeded(): Promise { const filesInDb = await loadMockFilePathsFromDb(); if (filesInDb.length > 0 || !fs.existsSync(MOCK_DIR)) return; if (!fs.existsSync(MOCK_DIR)) { return; } const files: string[] = []; walkMockFiles(MOCK_DIR, MOCK_DIR, files); files.sort((a, b) => a.localeCompare(b)); for (const filePath of files) { await upsertMockFilePathToDb(filePath); } } function readBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; req.on("data", (chunk: Buffer) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))); req.on("error", reject); }); } // 验证mock文件是否存在 function validateMockFiles() { console.log(`[CONFIG] 验证mock文件...`); let missingFiles: Array<{ route: string; filePath: string; fullPath: string; }> = []; for (const [route, filePath] of Object.entries(MOCK_ROUTES)) { const fullPath = path.join(__dirname, filePath); if (!fs.existsSync(fullPath)) { missingFiles.push({ route, filePath, fullPath }); console.warn( `[CONFIG] 警告: mock文件不存在 - ${filePath} (用于路由: ${route})`, ); } } if (missingFiles.length > 0) { console.log( `[CONFIG] 缺少 ${missingFiles.length} 个mock文件,请创建这些文件`, ); } else { console.log(`[CONFIG] 所有mock文件验证通过`); } } // 检查是否为mock路由的函数 function isMockRoute(requestPath: string): boolean { if (CONFIG.mockEnabled === false) return false; return MOCK_ROUTES.hasOwnProperty(requestPath); } // 获取mock文件路径 function getMockFilePath(requestPath: string): string { return MOCK_ROUTES[requestPath]; } function getMockStatusCode(requestPath: string): number { return normalizeStatusCode(MOCK_ROUTE_STATUSES[requestPath], 200); } function decodeBodyByEncoding( bodyBuffer: Buffer, contentEncoding?: string, ): Buffer { const encoding = (contentEncoding || "").toLowerCase().trim(); try { if (encoding.includes("gzip")) { return zlib.gunzipSync(bodyBuffer); } if (encoding.includes("br")) { return zlib.brotliDecompressSync(bodyBuffer); } if (encoding.includes("deflate")) { return zlib.inflateSync(bodyBuffer); } } catch (error) { console.warn("[MOCK] 解压上游响应失败,按原始内容写入文件", error); } return bodyBuffer; } // 代理服务器 const proxyServer = http.createServer(async (clientReq, clientRes) => { // 解析客户端请求的 URL const parsedUrl = new URL(`http://localhost${clientReq.url!}`); const requestPath = parsedUrl.pathname; // 管理接口:保留路径,不参与代理转发 if (requestPath === "/__reload-config") { if (clientReq.method !== "POST") { clientRes.writeHead(405, { "Content-Type": "application/json", Allow: "POST", }); clientRes.end( JSON.stringify({ success: false, error: "Method Not Allowed", allow: ["POST"], }), ); return; } try { const oldCount = Object.keys(MOCK_ROUTES).length; await loadConfig(); const newCount = Object.keys(MOCK_ROUTES).length; clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, message: "Configuration reloaded successfully", routesCount: newCount, routesChanged: newCount - oldCount, }), ); console.log( `[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`, ); } catch (error) { clientRes.writeHead(500, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: false, error: (error as Error).message, }), ); } return; } if (requestPath === "/__config") { if (clientReq.method !== "GET" && clientReq.method !== "POST") { clientRes.writeHead(405, { "Content-Type": "application/json", Allow: "GET, POST", }); clientRes.end( JSON.stringify({ success: false, error: "Method Not Allowed", allow: ["GET", "POST"], }), ); return; } if (clientReq.method === "POST") { readBody(clientReq) .then(async (bodyText) => { const body = bodyText ? JSON.parse(bodyText) : {}; const nextConfig: ConfigFile = { routes: body.routes ?? RAW_ROUTES, routeStatuses: body.routeStatuses ?? RAW_ROUTE_STATUSES, config: body.config || CONFIG, }; await saveConfigFile(nextConfig); validateMockFiles(); clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, message: "Configuration saved successfully", totalRoutes: Object.keys(MOCK_ROUTES).length, }), ); }) .catch((error) => { clientRes.writeHead(400, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: false, error: (error as Error).message, }), ); }); return; } try { clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify( { routes: RAW_ROUTES, routeStatuses: RAW_ROUTE_STATUSES, config: CONFIG, timestamp: new Date().toISOString(), totalRoutes: Object.keys(MOCK_ROUTES).length, }, null, 2, ), ); } catch (error) { clientRes.writeHead(500, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: false, error: (error as Error).message, }), ); } return; } if (requestPath === "/__routes") { if (clientReq.method !== "POST") { clientRes.writeHead(405, { "Content-Type": "application/json", Allow: "POST", }); clientRes.end( JSON.stringify({ success: false, error: "Method Not Allowed", allow: ["POST"], }), ); return; } readBody(clientReq) .then(async (bodyText) => { const body = bodyText ? JSON.parse(bodyText) : {}; let route = String(body.route || "").trim(); const filePath = String(body.filePath || "").trim(); const fileContent = String(body.fileContent || ""); const overwrite = body.overwrite === true; const template = String(body.template || "").trim(); const apiName = String(body.apiName || "").trim(); const selectedApiRoute = String(body.selectedApiRoute || "").trim(); const originalRoute = String(body.originalRoute || "").trim(); const originalRawRoute = String(body.originalRawRoute || "").trim(); const statusCode = normalizeStatusCode(body.statusCode, 200); const enabled = body.enabled !== false; const useExistingFile = body.useExistingFile === true || template === "basicError"; const apiList = await loadApiList(); const selectedApi = selectedApiRoute ? apiList.find((item) => item.route === selectedApiRoute) : undefined; const originalApi = originalRoute ? apiList.find((item) => item.route === originalRoute) : undefined; // 来自 api-list 的接口路由不可在新增/修改时变更 if (selectedApi) { if (route && route !== selectedApi.route) { throw new Error("api-list route cannot be changed"); } if (apiName && apiName !== selectedApi.name) { throw new Error("api-list name cannot be changed"); } route = selectedApi.route; } if (originalApi && route !== originalApi.route) { throw new Error("api-list route cannot be changed"); } if (originalApi && apiName && apiName !== originalApi.name) { throw new Error("api-list name cannot be changed"); } if (!route.startsWith("/")) { throw new Error("route must start with '/'"); } const normalizedFilePath = normalizeMockFilePath(filePath); const fullPath = resolveMockFullPath(normalizedFilePath); if (useExistingFile) { if (!fs.existsSync(fullPath)) { throw new Error("mock file does not exist"); } } else { if (!overwrite && fs.existsSync(fullPath)) { throw new Error( "mock file already exists, set overwrite=true to replace", ); } fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, fileContent, "utf-8"); } const nextConfig = buildCurrentConfigFile(); const nextRouteKey = enabled ? route : `#${route}`; const oldRouteCandidates = new Set(); if (originalRawRoute) { oldRouteCandidates.add(originalRawRoute); } if (originalRoute) { oldRouteCandidates.add(originalRoute); oldRouteCandidates.add(`#${originalRoute}`); } oldRouteCandidates.forEach((key) => { if (key && key !== nextRouteKey) { delete nextConfig.routes[key]; delete nextConfig.routeStatuses?.[key]; } }); nextConfig.routes[nextRouteKey] = normalizedFilePath; if (!nextConfig.routeStatuses) { nextConfig.routeStatuses = {}; } nextConfig.routeStatuses[nextRouteKey] = statusCode; await saveConfigFile(nextConfig); await upsertMockFilePathToDb(normalizedFilePath); clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, message: "Route and mock file created successfully", route, routeKey: nextRouteKey, filePath: normalizedFilePath, statusCode, enabled, }), ); }) .catch((error) => { clientRes.writeHead(400, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: false, error: (error as Error).message, }), ); }); return; } if (requestPath === "/__api-list") { if (clientReq.method !== "GET") { clientRes.writeHead(405, { "Content-Type": "application/json", Allow: "GET", }); clientRes.end( JSON.stringify({ success: false, error: "Method Not Allowed", allow: ["GET"], }), ); return; } clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, list: await loadApiList(), }), ); return; } if (requestPath === "/__mock-files") { if ( clientReq.method !== "GET" && clientReq.method !== "POST" && clientReq.method !== "DELETE" ) { clientRes.writeHead(405, { "Content-Type": "application/json", Allow: "GET, POST, DELETE", }); clientRes.end( JSON.stringify({ success: false, error: "Method Not Allowed", allow: ["GET", "POST", "DELETE"], }), ); return; } if (clientReq.method === "GET") { clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, list: await loadMockFiles(), }), ); return; } readBody(clientReq) .then(async (bodyText) => { const body = bodyText ? JSON.parse(bodyText) : {}; const filePath = String(body.filePath || "").trim(); const alias = String(body.alias || ""); const fullPath = resolveMockFullPath(filePath); const normalizedPath = normalizeMockFilePath(filePath); if (clientReq.method === "POST") { const content = String(body.content || ""); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content, "utf-8"); await upsertMockFilePathToDb(normalizedPath, alias); clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, filePath: normalizedPath, alias, }), ); return; } // DELETE if (!fs.existsSync(fullPath)) { throw new Error("mock file does not exist"); } fs.unlinkSync(fullPath); await removeMockFilePathFromDb(normalizedPath); clientRes.writeHead(200, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: true, filePath: normalizedPath, }), ); }) .catch((error) => { clientRes.writeHead(400, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ success: false, error: (error as Error).message, }), ); }); return; } if (requestPath === "/__admin") { if (clientReq.method !== "GET") { clientRes.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" }); clientRes.end("Method Not Allowed"); return; } const adminHtmlPath = path.join(__dirname, "admin.html"); if (!fs.existsSync(adminHtmlPath)) { clientRes.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); clientRes.end("admin.html not found"); return; } const html = fs.readFileSync(adminHtmlPath, "utf-8"); clientRes.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); clientRes.end(html); return; } // 检查是否为需要mock的路由 if (isMockRoute(requestPath)) { const mockFile = getMockFilePath(requestPath); const mockStatusCode = getMockStatusCode(requestPath); console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`); try { // 构建完整的文件路径 const mockFilePath = path.join(__dirname, mockFile); // 检查文件是否存在 if (!fs.existsSync(mockFilePath)) { console.warn(`[MOCK] Mock文件不存在,回源并自动生成: ${mockFilePath}`); const targetPort = getTargetPort(); const options: http.RequestOptions = { hostname: CONFIG.targetHost, port: targetPort, method: clientReq.method, path: parsedUrl.pathname + parsedUrl.search, headers: { ...clientReq.headers, host: CONFIG.targetHost, }, }; const proxyReq = upstreamRequest(options, (proxyRes) => { const chunks: Buffer[] = []; proxyRes.on("data", (chunk: Buffer) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }); proxyRes.on("end", () => { const bodyBuffer = Buffer.concat(chunks); try { const decodedBuffer = decodeBodyByEncoding( bodyBuffer, Array.isArray(proxyRes.headers["content-encoding"]) ? proxyRes.headers["content-encoding"][0] : proxyRes.headers["content-encoding"], ); const contentType = String(proxyRes.headers["content-type"] || ""); const toWrite = contentType.includes("application/json") || contentType.includes("text/") || contentType.includes("application/xml") || contentType.includes("application/javascript") ? decodedBuffer.toString("utf-8") : decodedBuffer; fs.mkdirSync(path.dirname(mockFilePath), { recursive: true }); fs.writeFileSync(mockFilePath, toWrite); console.log(`[MOCK] 已自动写入mock文件: ${mockFilePath}`); } catch (writeErr) { console.error(`[MOCK] 自动写入mock文件失败: ${mockFilePath}`, writeErr); } clientRes.writeHead(mockStatusCode, { ...proxyRes.headers, "X-Mock-Autogenerated": "true", "X-Mock-Source": mockFile, "X-Mock-Status-Code": String(mockStatusCode), }); clientRes.end(bodyBuffer); }); }); proxyReq.on("error", (err) => { console.error("Proxy request error:", err); clientRes.writeHead(500, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ error: "Proxy error", route: requestPath, file: mockFile, message: err.message, timestamp: new Date().toISOString(), }), ); }); clientReq.pipe(proxyReq); return; } // 读取本地mock文件 const mockData = fs.readFileSync(mockFilePath, "utf-8"); // 设置响应头 const contentType = CONFIG.defaultContentType || "application/json"; clientRes.writeHead(mockStatusCode, { "Content-Type": contentType, "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "X-Mock-Source": mockFile, "X-Mock-Status-Code": String(mockStatusCode), "X-Mock-Timestamp": new Date().toISOString(), }); // 返回mock数据 clientRes.end(mockData); console.log(`[MOCK] 成功返回mock数据: ${mockFile}`); } catch (error) { console.error(`[MOCK] 读取mock文件失败: ${mockFile}`, error); clientRes.writeHead(500, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ error: "Failed to read mock data", route: requestPath, file: mockFile, message: (error as Error).message, timestamp: new Date().toISOString(), }), ); } return; // 直接返回,不转发到目标服务器 } // 如果不是mock路由,正常代理转发 console.log(`[PROXY] 转发请求: ${requestPath}`); const targetPort = getTargetPort(); // 目标服务器的选项 const options: http.RequestOptions = { hostname: CONFIG.targetHost, port: targetPort, method: clientReq.method, path: parsedUrl.pathname + parsedUrl.search, headers: { ...clientReq.headers, host: CONFIG.targetHost, }, }; const proxyReq = upstreamRequest(options, (proxyRes) => { // 将目标服务器的响应头复制到客户端响应 clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers); // 将目标服务器的响应数据管道传输到客户端 proxyRes.pipe(clientRes); }); // 错误处理 proxyReq.on("error", (err) => { console.error("Proxy request error:", err); clientRes.writeHead(500, { "Content-Type": "application/json" }); clientRes.end( JSON.stringify({ error: "Proxy error", message: err.message, timestamp: new Date().toISOString(), }), ); }); // 将客户端请求体管道传输到代理请求 clientReq.pipe(proxyReq); }); function setupConfigWatcher(): void { if (!CONFIG.reloadOnChange) return; fs.watchFile(CONFIG_FILE, async () => { console.log(`[CONFIG] 配置文件已修改,重新加载...`); try { const oldRoutesCount = Object.keys(MOCK_ROUTES).length; await loadConfig(); const newRoutesCount = Object.keys(MOCK_ROUTES).length; console.log( `[CONFIG] 配置重载完成 (路由数: ${oldRoutesCount} -> ${newRoutesCount})`, ); } catch (error) { console.error(`[CONFIG] 重新加载配置文件失败:`, error); } }); console.log(`[CONFIG] 已启用配置文件监视: ${CONFIG_FILE}`); } function startServer(): void { proxyServer.listen(CONFIG.proxyPort, "0.0.0.0", () => { const targetPort = getTargetPort(); const proto = isTargetHttps() ? "https" : "http"; const defaultPort = isTargetHttps() ? 443 : 80; console.log(`========================================`); console.log(`代理服务器运行在: http://localhost:${CONFIG.proxyPort}`); console.log( `目标服务器: ${proto}://${CONFIG.targetHost}${targetPort !== defaultPort ? `:${targetPort}` : ""}`, ); console.log(`配置文件: ${CONFIG_FILE}`); console.log( `已配置Mock路由: ${Object.keys(MOCK_ROUTES).length} 个${CONFIG.mockEnabled !== false ? "" : "(当前 mockEnabled=false,未生效)"}`, ); console.log(`========================================`); console.log(`管理接口:`); console.log( ` GET http://localhost:${CONFIG.proxyPort}/__config 查看当前配置`, ); console.log( ` POST http://localhost:${CONFIG.proxyPort}/__reload-config 重新加载配置`, ); console.log(`========================================`); console.log(`Mock路由列表:`); for (const [route, file] of Object.entries(MOCK_ROUTES)) { const filePath = path.join(__dirname, file); const exists = fs.existsSync(filePath) ? "✓" : "✗"; console.log(` ${exists} ${route} -> ${file}`); } console.log(`========================================`); }); } async function bootstrap(): Promise { await initDatabase(); await initMockFilesFromFsIfNeeded(); await loadConfig(); const apiListFromDb = await loadApiListFromDb(); if (apiListFromDb.length === 0) { await upsertApiListToDb(loadApiListFromJson()); } setupConfigWatcher(); startServer(); } bootstrap().catch((error) => { console.error("[BOOTSTRAP] 启动失败:", error); process.exit(1); });