feat: 重构代理功能,添加文件代理支持并优化相关逻辑

This commit is contained in:
2026-01-18 13:41:47 +08:00
parent 5e5f4f6543
commit 43992d896f
11 changed files with 166 additions and 63 deletions

View File

@@ -4,12 +4,13 @@ import fs from 'fs';
import { checkFileExists, createDir } from '../file/index.ts';
import { ProxyInfo } from '../proxy/proxy.ts';
import dotenv from 'dotenv';
import { logger } from '@/module/logger.ts';
let kevisualDir = path.join(homedir(), 'kevisual');
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
if (envKevisualDir) {
kevisualDir = envKevisualDir;
console.log('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
logger.debug('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
}
/**
* 助手配置文件路径, 全局配置文件目录
@@ -191,10 +192,11 @@ export class AssistantConfig {
}
return this.#configPath;
}
init() {
this.configPath = initConfig(this.configDir);
init(configDir?: string) {
this.configPath = initConfig(configDir || this.configDir);
this.isMountedConfig = true;
}
checkMounted() {
if (!this.isMountedConfig) {
this.init();
@@ -356,7 +358,7 @@ type AppConfig = {
list: any[];
};
export function parseArgs(args: string[]) {
const result: { root?: string; home?: boolean; help?: boolean } = {};
const result: { root?: string; home?: boolean; help?: boolean } = { home: true };
for (let i = 0; i < args.length; i++) {
const arg = args[i];
// 处理 root 参数
@@ -366,14 +368,13 @@ export function parseArgs(args: string[]) {
i++; // 跳过下一个参数,因为它是值
}
}
// 处理 home 参数
// if (arg === '--home') {
result.home = true;
// }
if (arg === '--help' || arg === '-h') {
result.help = true;
}
}
if (result.root) {
result.home = false;
}
return result;
}
/**

View File

@@ -35,7 +35,8 @@ export const checkFileDir = (filePath: string, create = true) => {
return exist;
};
export const createDir = (dirPath: string) => {
export const createDir = (dirPath: string, isCreate = true) => {
if (!isCreate) return dirPath;
if (!checkFileExists(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}

View File

@@ -5,7 +5,8 @@ import path from 'node:path';
import { ProxyInfo } from './proxy.ts';
import { checkFileExists } from '../file/index.ts';
import { log } from '@/module/logger.ts';
import { pipeFileStream } from './pipe.ts';
import { getContentType } from './module/mime.ts';
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
// url开头的文件
const url = new URL(req.url, 'http://localhost');
@@ -51,3 +52,37 @@ export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, p
return;
}
};
export const fileProxy2 = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
res.statusCode = 501;
const url = new URL(req.url, 'http://localhost');
const { rootPath, indexPath = '' } = proxyApi?.file || {}
if (!rootPath) {
res.end(`系统未配置根路径 rootPath id:[${proxyApi?.file?.id}]`);
return;
}
const pathname = url.pathname;
let targetFilepath = pathname.replace(proxyApi.path || '', '');
if (targetFilepath.endsWith('/')) {
// 没有指定文件访问index.html
targetFilepath += 'index.html';
}
const filePath = path.join(rootPath || process.cwd(), targetFilepath);
const indexTargetPath = path.join(rootPath || process.cwd(), indexPath);
let sendPath = filePath;
if (!checkFileExists(filePath)) {
res.setHeader('X-Proxy-File', 'false');
if (indexPath && checkFileExists(indexTargetPath)) {
sendPath = indexTargetPath;
} else {
res.statusCode = 404;
res.end(`文件不存在, 路径: ${filePath}`);
return;
}
} else {
res.setHeader('X-Proxy-File', 'true');
}
const contentType = getContentType(sendPath);
res.setHeader('Content-Type', contentType);
pipeFileStream(sendPath, res);
};

View File

@@ -0,0 +1,50 @@
import path from 'path';
// 获取文件的 content-type
export const getContentType = (filePath: string) => {
const extname = path.extname(filePath);
const contentType = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.md': 'text/markdown; charset=utf-8', // utf-8配置
'.ico': 'image/x-icon', // Favicon 图标
'.webp': 'image/webp', // WebP 图像格式
'.webm': 'video/webm', // WebM 视频格式
'.ogg': 'audio/ogg', // Ogg 音频格式
'.mp3': 'audio/mpeg', // MP3 音频格式
'.m4a': 'audio/mp4', // M4A 音频格式
'.m3u8': 'application/vnd.apple.mpegurl', // HLS 播放列表
'.ts': 'video/mp2t', // MPEG Transport Stream
'.pdf': 'application/pdf', // PDF 文档
'.doc': 'application/msword', // Word 文档
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Word 文档 (新版)
'.ppt': 'application/vnd.ms-powerpoint', // PowerPoint 演示文稿
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // PowerPoint (新版)
'.xls': 'application/vnd.ms-excel', // Excel 表格
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // Excel 表格 (新版)
'.csv': 'text/csv; charset=utf-8', // CSV 文件
'.xml': 'application/xml; charset=utf-8', // XML 文件
'.rtf': 'application/rtf', // RTF 文本文件
'.eot': 'application/vnd.ms-fontobject', // Embedded OpenType 字体
'.ttf': 'font/ttf', // TrueType 字体
'.woff': 'font/woff', // Web Open Font Format 1.0
'.woff2': 'font/woff2', // Web Open Font Format 2.0
'.otf': 'font/otf', // OpenType 字体
'.wasm': 'application/wasm', // WebAssembly 文件
'.pem': 'application/x-pem-file', // PEM 证书文件
'.crt': 'application/x-x509-ca-cert', // CRT 证书文件
'.yaml': 'application/x-yaml; charset=utf-8', // YAML 文件
'.yml': 'application/x-yaml; charset=utf-8', // YAML 文件(别名)
'.zip': 'application/octet-stream',
};
return contentType[extname] || 'application/octet-stream';
};

View File

@@ -2,6 +2,7 @@ import * as http from 'http';
import * as fs from 'fs';
import { isBun } from './utils.ts';
import { Readable } from 'stream';
import { logger } from '@/module/logger.ts';
/**
* 文件流管道传输函数
* 将指定文件的内容通过流的方式传输给客户端响应
@@ -99,7 +100,7 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
proxyReq.end();
return;
}
console.log('Bun pipeProxyReq content-type', contentType);
logger.debug('Bun pipeProxyReq content-type', contentType);
// @ts-ignore
const bodyString = req.body;
bodyString && proxyReq.write(bodyString);

View File

@@ -1,6 +1,7 @@
import http from 'node:http';
import { httpProxy } from './http-proxy.ts';
import { s3Proxy } from './s3.ts';
import { fileProxy2 } from './file-proxy.ts';
export type ProxyInfo = {
/**
* 代理路径, 比如/root/home, 匹配的路径
@@ -45,6 +46,11 @@ export type ProxyInfo = {
accessKeyId?: string;
secretAccessKey?: string;
endpoint?: string;
},
file?: {
id?: string;
indexPath?: string;
rootPath?: string;
}
};
@@ -56,4 +62,7 @@ export const proxy = (req: http.IncomingMessage, res: http.ServerResponse, proxy
if (proxyApi.type === 's3') {
return s3Proxy(req, res, proxyApi);
}
if (proxyApi.type === 'file') {
return fileProxy2(req, res, proxyApi);
}
}

View File

@@ -17,8 +17,11 @@ type ProxyType = {
user: string;
key: string;
path: string;
indexPath: string;
absolutePath?: string;
type?: 'file';
file: {
indexPath: string;
absolutePath: string;
};
};
export type LocalProxyOpts = {
watch?: boolean; // 是否监听文件变化
@@ -79,8 +82,10 @@ export class LocalProxy {
user: user,
key: app,
path: `/${user}/${app}/`,
indexPath: `${user}/${app}/index.html`,
absolutePath: appPath,
file: {
indexPath: `${user}/${app}/index.html`,
absolutePath: appPath,
}
});
}
});