diff --git a/assistant/src/command/config-manager/index.ts b/assistant/src/command/config-manager/index.ts index cdef122..caf51ac 100644 --- a/assistant/src/command/config-manager/index.ts +++ b/assistant/src/command/config-manager/index.ts @@ -6,19 +6,19 @@ import inquirer from 'inquirer'; import chalk from 'chalk'; type InitCommandOptions = { - path?: string; + workspace?: string; }; const Init = new Command('init') .description('初始化一个助手客户端,生成配置文件。') - .option('-p --path ', '助手路径,默认为执行命令的目录,如果助手路径不存在则创建。') + .option('-w --workspace ', '助手路径,默认为执行命令的目录,如果助手路径不存在则创建。') .action((opts: InitCommandOptions) => { - // 如果path参数存在,检测path是否是相对路径,如果是相对路径,则转换为绝对路径 - if (opts.path && !opts.path.startsWith('/')) { - opts.path = path.join(process.cwd(), opts.path); - } else if (opts.path) { - opts.path = path.resolve(opts.path); + // 如果workspace参数存在,检测workspace是否是相对路径,如果是相对路径,则转换为绝对路径 + if (opts.workspace && !opts.workspace.startsWith('/')) { + opts.workspace = path.join(process.cwd(), opts.workspace); + } else if (opts.workspace) { + opts.workspace = path.resolve(opts.workspace); } - const configDir = AssistantInit.detectConfigDir(opts.path); + const configDir = AssistantInit.detectConfigDir(opts.workspace); console.log('configDir', configDir); const assistantInit = new AssistantInit({ path: configDir, diff --git a/assistant/src/module/assistant/config/index.ts b/assistant/src/module/assistant/config/index.ts index 3c001fe..af6d761 100644 --- a/assistant/src/module/assistant/config/index.ts +++ b/assistant/src/module/assistant/config/index.ts @@ -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; } /** diff --git a/assistant/src/module/assistant/file/index.ts b/assistant/src/module/assistant/file/index.ts index 922d74f..f9b6337 100644 --- a/assistant/src/module/assistant/file/index.ts +++ b/assistant/src/module/assistant/file/index.ts @@ -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 }); } diff --git a/assistant/src/module/assistant/proxy/file-proxy.ts b/assistant/src/module/assistant/proxy/file-proxy.ts index 1aa2f03..51168a1 100644 --- a/assistant/src/module/assistant/proxy/file-proxy.ts +++ b/assistant/src/module/assistant/proxy/file-proxy.ts @@ -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); +}; \ No newline at end of file diff --git a/assistant/src/module/assistant/proxy/module/mime.ts b/assistant/src/module/assistant/proxy/module/mime.ts new file mode 100644 index 0000000..e7d619d --- /dev/null +++ b/assistant/src/module/assistant/proxy/module/mime.ts @@ -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'; +}; diff --git a/assistant/src/module/assistant/proxy/pipe.ts b/assistant/src/module/assistant/proxy/pipe.ts index 800dc7b..386f267 100644 --- a/assistant/src/module/assistant/proxy/pipe.ts +++ b/assistant/src/module/assistant/proxy/pipe.ts @@ -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); diff --git a/assistant/src/module/assistant/proxy/proxy.ts b/assistant/src/module/assistant/proxy/proxy.ts index 7f8891b..3180424 100644 --- a/assistant/src/module/assistant/proxy/proxy.ts +++ b/assistant/src/module/assistant/proxy/proxy.ts @@ -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); + } } \ No newline at end of file diff --git a/assistant/src/module/local-proxy/index.ts b/assistant/src/module/local-proxy/index.ts index a233fc2..afc59dd 100644 --- a/assistant/src/module/local-proxy/index.ts +++ b/assistant/src/module/local-proxy/index.ts @@ -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, + } }); } }); diff --git a/assistant/src/server.ts b/assistant/src/server.ts index 68433bf..7fa06b8 100644 --- a/assistant/src/server.ts +++ b/assistant/src/server.ts @@ -69,6 +69,7 @@ program .option('-n, --name ', '服务名称', 'assistant-server') .option('-p, --port ', '服务端口') .option('-s, --start', '是否启动服务') + .option('-r, --root ', '工作空间路径') .option('-e, --interpreter ', '指定使用的解释器', 'bun') .action(async (options) => { // console.log('当前执行路径:', execPath, inte); @@ -99,7 +100,9 @@ program if (port) { pm2Command += ` -p ${port}`; } - + if (options.root) { + pm2Command += ` --root ${options.root}`; + } console.log(chalk.gray('执行命令:'), pm2Command); console.log(chalk.gray('脚本路径:'), runPath); diff --git a/assistant/src/services/init/index.ts b/assistant/src/services/init/index.ts index d94658b..5a59ae7 100644 --- a/assistant/src/services/init/index.ts +++ b/assistant/src/services/init/index.ts @@ -27,13 +27,16 @@ export class AssistantInit extends AssistantConfig { } } - async init() { + async init(configDir?: string) { + if (configDir) { + this.configDir = configDir; + } // 1. 检查助手路径是否存在 if (!this.checkConfigPath()) { console.log(chalk.blue('助手路径不存在,正在创建...')); - super.init(); + super.init(configDir); } else { - super.init(); + super.init(configDir); const assistantConfig = this; console.log(chalk.yellow('助手路径已存在'), chalk.green(assistantConfig.configDir)); } diff --git a/assistant/src/services/proxy/proxy-page-index.ts b/assistant/src/services/proxy/proxy-page-index.ts index 1b6be22..c7e4587 100644 --- a/assistant/src/services/proxy/proxy-page-index.ts +++ b/assistant/src/services/proxy/proxy-page-index.ts @@ -1,4 +1,4 @@ -import { fileProxy, httpProxy, createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts'; +import { fileProxy, httpProxy, createApiProxy, ProxyInfo, proxy, fileProxy2 } from '@/module/assistant/index.ts'; import http from 'node:http'; import { LocalProxy } from './local-proxy.ts'; import { assistantConfig, app, simpleRouter } from '@/app.ts'; @@ -116,27 +116,37 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn'); const allProxy = [...apiProxy, ...defaultApiProxy]; const apiBackendProxy = allProxy.find((item) => pathname.startsWith(item.path)); - // console.log('apiBackendProxy', allProxy, apiBackendProxy, pathname, apiProxy[0].path); - if (apiBackendProxy) { - log.debug('apiBackendProxy', { apiBackendProxy, url: req.url }); + const proxyFn = async (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => { + log.debug('proxyApi', { proxyApi, url: req.url }); // 设置 CORS 头 // res.setHeader('Access-Control-Allow-Origin', '*'); // res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); // res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With'); - if (apiBackendProxy.s3?.id) { + if (proxyApi.s3?.id) { const storage = _assistantConfig?.storage || [] - const storageConfig = storage.find((item) => item.id === apiBackendProxy.s3?.id); - apiBackendProxy.s3 = { + const storageConfig = storage.find((item) => item.id === proxyApi.s3?.id); + proxyApi.s3 = { ...storageConfig, - ...apiBackendProxy.s3, + ...proxyApi.s3, + } + } + if (proxyApi.file?.id) { + const storage = _assistantConfig?.storage || [] + const storageConfig = storage.find((item) => item.id === proxyApi.file?.id); + proxyApi.file = { + ...storageConfig, + ...proxyApi.file, } } return proxy(req, res, { - path: apiBackendProxy.path, - target: apiBackendProxy.target, - ...apiBackendProxy, + path: proxyApi.path, + target: proxyApi.target, + ...proxyApi, }); } + if (apiBackendProxy) { + return proxyFn(req, res, apiBackendProxy); + } logger.debug('proxyRoute handle by router', { url: req.url }, noAdmin); const urls = pathname.split('/'); const [_, _user, _app] = urls; @@ -157,27 +167,9 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp } const proxyApiList = _assistantConfig?.proxy || []; const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path)); - if (proxyApi && proxyApi.type === 'file') { - log.debug('proxyApi', { proxyApi, pathname }); - const _indexPath = proxyApi.indexPath || `${_user}/${_app}/index.html`; - const _rootPath = proxyApi.rootPath; - if (!_rootPath) { - log.error('Not Found rootPath', { proxyApi, pathname }); - return res.end(`Not Found [${proxyApi.path}] rootPath`); - } - return fileProxy(req, res, { - path: proxyApi.path, // 代理路径, 比如/root/home - rootPath: proxyApi.rootPath, - ...proxyApi, - indexPath: _indexPath, // 首页路径 - }); - } else if (proxyApi && proxyApi.type === 'http') { - log.debug('proxyApi http', { proxyApi, pathname }); - return httpProxy(req, res, { - path: proxyApi.path, - target: proxyApi.target, - type: 'http', - }); + if (proxyApi) { + log.debug('proxyPage', { proxyApi, pathname }); + return proxyFn(req, res, proxyApi); } const filter = await authFilter(req, res); if (filter.code !== 200) { @@ -189,16 +181,19 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path)); if (localProxyProxy) { log.log('localProxyProxy', { localProxyProxy, url: req.url }); - return fileProxy(req, res, { + return proxyFn(req, res, { path: localProxyProxy.path, - rootPath: localProxy.pagesDir, - indexPath: localProxyProxy.indexPath, + "type": 'file', + file: { + rootPath: localProxy.pagesDir, + indexPath: localProxyProxy.file.indexPath, + } }); } const creatCenterProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn', ['/root', '/' + _user]); const centerProxy = creatCenterProxy.find((item) => pathname.startsWith(item.path)); if (centerProxy) { - return httpProxy(req, res, { + return proxyFn(req, res, { path: centerProxy.path, target: centerProxy.target, type: 'http',