api-proxy-mock/index.api.ts
2026-04-23 18:02:34 +08:00

904 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
// 配置文件路径
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");
// 定义类型
interface RouteConfig {
[route: string]: string;
}
interface AppConfig {
/** 为 false 时所有请求走代理,不命中 mock 路由;缺省为 true */
mockEnabled?: boolean;
cacheConfig: boolean;
reloadOnChange: boolean;
defaultContentType: string;
proxyPort: number;
targetHost: string;
/** 代理转发的目标端口;缺省 HTTPS 为 443HTTP 为 80 */
targetPort?: number;
/** 为 false 时用 HTTP 连接上游;缺省 trueHTTPS */
targetHttps?: boolean;
}
interface ConfigFile {
routes: RouteConfig;
config: AppConfig;
}
interface ApiListItem {
name: string;
route: string;
}
interface MockFileItem {
filePath: string;
content: string;
}
// 存储当前的路由配置
let MOCK_ROUTES: RouteConfig = {};
let RAW_ROUTES: RouteConfig = {};
let CONFIG: AppConfig = {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json",
proxyPort: 443,
targetHost: "localhost",
targetPort: 443,
};
// 过滤掉以 # 开头的路由(视为注释,不参与 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 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 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}`);
// 加载配置(# 开头的路由视为注释,不参与 mock
const configData: ConfigFile = JSON.parse(
fs.readFileSync(CONFIG_FILE, "utf-8"),
);
RAW_ROUTES = configData.routes || {};
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
CONFIG = configData.config || CONFIG;
} else {
const configData: ConfigFile = JSON.parse(
fs.readFileSync(CONFIG_FILE, "utf-8"),
);
RAW_ROUTES = configData.routes || {};
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
CONFIG = configData.config || CONFIG;
console.log(
`[CONFIG] 配置文件已加载,共 ${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 };
CONFIG = {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json",
proxyPort: 9443,
targetHost: "devrmtapp.resmart.cn",
targetPort: 443,
};
}
}
function buildCurrentConfigFile(): ConfigFile {
return {
routes: { ...RAW_ROUTES },
config: { ...CONFIG },
};
}
function saveConfigFile(nextConfig: ConfigFile): void {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), "utf-8");
RAW_ROUTES = nextConfig.routes || {};
MOCK_ROUTES = filterActiveRoutes(RAW_ROUTES);
CONFIG = nextConfig.config || CONFIG;
}
function loadApiList(): 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<string>();
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 [];
}
}
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;
const relative = path.relative(baseDir, fullPath).replace(/\\/g, "/");
result.push(`mock/${relative}`);
}
}
function loadMockFiles(): MockFileItem[] {
if (!fs.existsSync(MOCK_DIR)) {
return [];
}
const files: string[] = [];
walkMockFiles(MOCK_DIR, MOCK_DIR, files);
files.sort((a, b) => a.localeCompare(b));
return files.map((filePath) => {
const fullPath = path.join(__dirname, filePath);
return {
filePath,
content: fs.readFileSync(fullPath, "utf-8"),
};
});
}
function readBody(req: http.IncomingMessage): Promise<string> {
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 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((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;
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((bodyText) => {
const body = bodyText ? JSON.parse(bodyText) : {};
const nextConfig: ConfigFile = {
routes: body.routes || {},
config: body.config || CONFIG,
};
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,
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((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 enabled = body.enabled !== false;
const useExistingFile =
body.useExistingFile === true || template === "basicError";
const apiList = 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<string>();
if (originalRawRoute) {
oldRouteCandidates.add(originalRawRoute);
}
if (originalRoute) {
oldRouteCandidates.add(originalRoute);
oldRouteCandidates.add(`#${originalRoute}`);
}
oldRouteCandidates.forEach((key) => {
if (key && key !== nextRouteKey) {
delete nextConfig.routes[key];
}
});
nextConfig.routes[nextRouteKey] = normalizedFilePath;
saveConfigFile(nextConfig);
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,
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: 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: loadMockFiles(),
}),
);
return;
}
readBody(clientReq)
.then((bodyText) => {
const body = bodyText ? JSON.parse(bodyText) : {};
const filePath = String(body.filePath || "").trim();
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");
clientRes.writeHead(200, { "Content-Type": "application/json" });
clientRes.end(
JSON.stringify({
success: true,
filePath: normalizedPath,
}),
);
return;
}
// DELETE
if (!fs.existsSync(fullPath)) {
throw new Error("mock file does not exist");
}
fs.unlinkSync(fullPath);
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);
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(proxyRes.statusCode || 200, {
...proxyRes.headers,
"X-Mock-Autogenerated": "true",
"X-Mock-Source": mockFile,
});
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(200, {
"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-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);
});
// 初始化:第一次加载配置
loadConfig();
// 如果配置了文件监视,则监听配置文件变化
if (CONFIG.reloadOnChange) {
fs.watchFile(CONFIG_FILE, (curr, prev) => {
console.log(`[CONFIG] 配置文件已修改,重新加载...`);
try {
const oldRoutesCount = Object.keys(MOCK_ROUTES).length;
loadConfig();
const newRoutesCount = Object.keys(MOCK_ROUTES).length;
console.log(
`[CONFIG] 配置重载完成 (路由数: ${oldRoutesCount} -> ${newRoutesCount})`,
);
} catch (error) {
console.error(`[CONFIG] 重新加载配置文件失败:`, error);
}
});
console.log(`[CONFIG] 已启用配置文件监视: ${CONFIG_FILE}`);
}
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(`========================================`);
});