diff --git a/apps/jingrow/frontend/build-tool.js b/apps/jingrow/frontend/build-tool.js new file mode 100644 index 0000000..4fa1a19 --- /dev/null +++ b/apps/jingrow/frontend/build-tool.js @@ -0,0 +1,208 @@ +#!/usr/bin/env node +/** + * 单独构建工具脚本 + * 用法: node build-tool.js + * 示例: node build-tool.js remove_background + * + * 构建输出: + * - dist/tools/{tool_name}/assets/ 包含构建后的 JS 和 CSS 文件 + * - dist/tools/{tool_name}/manifest.json 包含构建文件清单 + */ + +import { build } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' +import Icons from 'unplugin-icons/vite' +import IconsResolver from 'unplugin-icons/resolver' +import Components from 'unplugin-vue-components/vite' +import path from 'node:path' +import fs from 'node:fs' + +const currentDir = fileURLToPath(new URL('.', import.meta.url)) +const srcDir = path.resolve(currentDir, 'src') +const toolsDir = path.resolve(srcDir, 'views', 'tools') + +// 获取工具名称 +const toolName = process.argv[2] + +if (!toolName) { + console.error('错误: 请提供工具名称') + console.log('用法: node build-tool.js ') + console.log('示例: node build-tool.js remove_background') + process.exit(1) +} + +const toolDir = path.resolve(toolsDir, toolName) +const toolVueFile = path.resolve(toolDir, `${toolName}.vue`) + +// 检查工具是否存在 +if (!fs.existsSync(toolVueFile)) { + console.error(`错误: 工具 "${toolName}" 不存在`) + console.error(`路径: ${toolVueFile}`) + process.exit(1) +} + +console.log(`开始构建工具: ${toolName}`) +console.log(`工具目录: ${toolDir}`) + +// 创建临时入口文件 +const tempEntryDir = path.resolve(currentDir, 'tmp', 'tool-build') +const tempEntryFile = path.resolve(tempEntryDir, `${toolName}.js`) +const tempIndexHtml = path.resolve(tempEntryDir, 'index.html') + +// 确保临时目录存在 +fs.mkdirSync(tempEntryDir, { recursive: true }) + +// 生成入口文件内容 +// 注意:构建为 ES 模块,导出组件供动态 import 使用 +const entryContent = `import Component from '../../src/views/tools/${toolName}/${toolName}.vue' + +// 导出组件供外部动态加载 +export default Component +` + +// 生成临时 HTML 文件 +const htmlContent = ` + + + + + ${toolName} + + +
+ + + +` + +fs.writeFileSync(tempEntryFile, entryContent, 'utf-8') +fs.writeFileSync(tempIndexHtml, htmlContent, 'utf-8') + +// 输出目录 +const outputDir = path.resolve(currentDir, 'dist', 'tools', toolName) + +// 构建配置(使用与主应用相同的配置) +const buildConfig = { + plugins: [ + vue(), + Icons({ + autoInstall: true, + compiler: 'vue3' + }), + Components({ + resolvers: [ + IconsResolver({ + prefix: 'i', + enabledCollections: ['tabler'] + }), + ], + }), + ], + resolve: { + alias: { + '@': path.resolve(currentDir, 'src'), + } + }, + build: { + outDir: outputDir, + assetsDir: 'assets', + rollupOptions: { + input: tempEntryFile, + output: { + format: 'es', // 使用 ES 模块格式,便于动态 import + // 使用工具名称作为文件名前缀,便于识别 + entryFileNames: `assets/${toolName}-[hash].js`, + chunkFileNames: `assets/[name]-[hash].js`, + assetFileNames: (assetInfo) => { + const ext = path.extname(assetInfo.name || '') + if (ext === '.css') { + return `assets/${toolName}-[hash].css` + } + return `assets/[name]-[hash]${ext}` + } + } + }, + sourcemap: false, + minify: 'terser', + terserOptions: { + compress: { + drop_console: true + } + } + } +} + +// 执行构建 +try { + console.log('正在构建...') + await build(buildConfig) + + // 生成 manifest 文件 + const manifest = { + toolName: toolName, + buildTime: new Date().toISOString(), + files: [] + } + + // 扫描输出目录,记录所有文件 + const assetsDir = path.resolve(outputDir, 'assets') + if (fs.existsSync(assetsDir)) { + const scanDir = (dir, basePath = '') => { + const files = fs.readdirSync(dir) + files.forEach(file => { + const filePath = path.resolve(dir, file) + const relativePath = path.join(basePath, file) + if (fs.statSync(filePath).isFile()) { + const stats = fs.statSync(filePath) + manifest.files.push({ + path: relativePath, + size: stats.size, + name: path.basename(filePath) + }) + } else if (fs.statSync(filePath).isDirectory()) { + scanDir(filePath, relativePath) + } + }) + } + scanDir(assetsDir, 'assets') + } + + // 保存 manifest + const manifestPath = path.resolve(outputDir, 'manifest.json') + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8') + + console.log(`\n✓ 构建成功!`) + console.log(`输出目录: ${outputDir}`) + console.log(`构建文件:`) + manifest.files.forEach(file => { + console.log(` - ${file.path} (${(file.size / 1024).toFixed(2)} KB)`) + }) + console.log(`\nManifest 文件: ${manifestPath}`) + + // 清理临时文件 + if (fs.existsSync(tempEntryFile)) { + fs.unlinkSync(tempEntryFile) + } + if (fs.existsSync(tempIndexHtml)) { + fs.unlinkSync(tempIndexHtml) + } + if (fs.existsSync(tempEntryDir) && fs.readdirSync(tempEntryDir).length === 0) { + fs.rmdirSync(tempEntryDir) + } + + console.log(`\n提示: 构建文件位于 dist/tools/${toolName}/assets/`) + console.log(`可以将此目录复制到工具包的 frontend/dist/ 目录中`) + +} catch (error) { + console.error('构建失败:', error) + // 清理临时文件 + if (fs.existsSync(tempEntryFile)) { + fs.unlinkSync(tempEntryFile) + } + if (fs.existsSync(tempIndexHtml)) { + fs.unlinkSync(tempIndexHtml) + } + process.exit(1) +} + diff --git a/apps/jingrow/frontend/package.json b/apps/jingrow/frontend/package.json index 5443d84..1d8f09a 100644 --- a/apps/jingrow/frontend/package.json +++ b/apps/jingrow/frontend/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:tool": "node build-tool.js", "preview": "vite preview" }, "dependencies": { diff --git a/apps/jingrow/frontend/src/shared/utils/dynamicRoutes.ts b/apps/jingrow/frontend/src/shared/utils/dynamicRoutes.ts index fa7d09c..d188f9e 100644 --- a/apps/jingrow/frontend/src/shared/utils/dynamicRoutes.ts +++ b/apps/jingrow/frontend/src/shared/utils/dynamicRoutes.ts @@ -29,6 +29,56 @@ export function ensureToolRoutes(tool: Tool): Tool { return tool } +/** + * 检测是否为生产环境 + */ +function isProduction(): boolean { + return import.meta.env.MODE === 'production' || import.meta.env.PROD +} + +/** + * 加载工具的独立构建文件(生产环境) + */ +async function loadToolBuiltComponent(toolName: string): Promise { + try { + // 尝试加载 manifest 文件 + const manifestUrl = `/tools/${toolName}/manifest.json` + const manifestResponse = await fetch(manifestUrl) + + if (!manifestResponse.ok) { + throw new Error(`Manifest not found: ${manifestUrl}`) + } + + const manifest = await manifestResponse.json() + + // 查找 JS 文件 + const jsFile = manifest.files?.find((f: any) => f.path.endsWith('.js')) + if (!jsFile) { + throw new Error('No JS file found in manifest') + } + + // 动态加载 JS 文件 + const jsUrl = `/tools/${toolName}/${jsFile.path}` + const module = await import(/* @vite-ignore */ jsUrl) + + // 查找 CSS 文件并加载 + const cssFile = manifest.files?.find((f: any) => f.path.endsWith('.css')) + if (cssFile) { + const cssUrl = `/tools/${toolName}/${cssFile.path}` + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = cssUrl + document.head.appendChild(link) + } + + // 返回组件(假设构建文件导出默认组件) + return module.default || module + } catch (error) { + console.error(`Failed to load built tool component: ${toolName}`, error) + throw error + } +} + export function registerToolRoute( router: Router, tool: Tool, @@ -58,11 +108,28 @@ export function registerToolRoute( return `tools/${toolDirName.replace(/_/g, '-')}` })() + const toolName = toolWithRoutes.toolName || toolWithRoutes.id + const route: RouteRecordRaw = { path: routePath, name: toolWithRoutes.routeName, - component: () => { - return import(finalComponentPath).catch((error) => { + component: async () => { + // 生产环境:优先尝试加载独立构建文件 + if (isProduction() && toolName) { + try { + const builtComponent = await loadToolBuiltComponent(toolName) + return builtComponent + } catch (builtError) { + console.warn(`Failed to load built tool ${toolName}, falling back to source:`, builtError) + // 如果加载构建文件失败,回退到源码加载 + } + } + + // 开发环境或构建文件加载失败:加载源码 + try { + return await import(finalComponentPath) + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error) console.error(`Failed to load tool component: ${finalComponentPath}`, error) return { name: 'ToolComponentError', @@ -70,11 +137,11 @@ export function registerToolRoute(

Component Not Found

Failed to load component: ${finalComponentPath}

-

Error: ${error.message || error}

+

Error: ${errorMessage}

` } - }) + } }, meta: { requiresAuth: true, diff --git a/apps/jingrow/jingrow/api/tools.py b/apps/jingrow/jingrow/api/tools.py index f81c499..84c430f 100644 --- a/apps/jingrow/jingrow/api/tools.py +++ b/apps/jingrow/jingrow/api/tools.py @@ -261,8 +261,20 @@ def _install_tool_from_file(file_path: str, tool_data: Dict[str, Any]) -> Dict[s return {'success': False, 'error': str(e)} +def _is_production_environment() -> bool: + """检测是否为生产环境(检查dist目录是否存在)""" + jingrow_root = get_jingrow_root() + frontend_dist = jingrow_root.parent / "frontend" / "dist" + return frontend_dist.exists() and frontend_dist.is_dir() + + def _install_single_tool_directory(tool_dir: str, tool_data: Dict[str, Any]) -> Dict[str, Any]: - """安装单个工具目录(每个工具独立)""" + """安装单个工具目录(每个工具独立) + + 支持两种模式: + 1. 开发环境:复制源码到 src/views/tools/{tool_name}/ + 2. 生产环境:如果工具包包含构建文件,复制到 dist/assets/ + """ try: tool_dir_path = Path(tool_dir) @@ -271,21 +283,9 @@ def _install_single_tool_directory(tool_dir: str, tool_data: Dict[str, Any]) -> if not tool_name: return {'success': False, 'error': '工具元数据中缺少 tool_name'} - # 确定目标目录:apps/jingrow/frontend/src/views/tools/{tool_name} jingrow_root = get_jingrow_root() - frontend_root = jingrow_root.parent / "frontend" / "src" - tool_frontend_dir = frontend_root / "views" / "tools" / tool_name - tool_frontend_dir.mkdir(parents=True, exist_ok=True) - - # 复制前端组件文件(如果存在) - # 结构:tool_name/frontend/tool_name/tool_name.vue - frontend_source = tool_dir_path / "frontend" / tool_name - if frontend_source.exists() and frontend_source.is_dir(): - # 复制 frontend/tool_name/ 目录下的所有内容到目标目录 - if tool_frontend_dir.exists(): - shutil.rmtree(tool_frontend_dir) - shutil.copytree(frontend_source, tool_frontend_dir) - logger.info(f"复制前端组件目录: {frontend_source} -> {tool_frontend_dir}") + frontend_root = jingrow_root.parent / "frontend" + is_production = _is_production_environment() # 复制后端文件(如果存在) # 结构:tool_name/backend/tool_name/tool_name.py @@ -298,11 +298,74 @@ def _install_single_tool_directory(tool_dir: str, tool_data: Dict[str, Any]) -> shutil.copytree(backend_source, backend_target) logger.info(f"复制后端文件目录: {backend_source} -> {backend_target}") + # 处理前端文件 + if is_production: + # 生产环境:优先使用构建文件 + frontend_dist_source = tool_dir_path / "frontend" / "dist" + if frontend_dist_source.exists() and frontend_dist_source.is_dir(): + # 每个工具使用独立的目录:dist/tools/{tool_name}/assets/ + tool_dist_dir = frontend_root / "dist" / "tools" / tool_name + tool_assets_dir = tool_dist_dir / "assets" + tool_assets_dir.mkdir(parents=True, exist_ok=True) + + # 复制构建文件到独立目录 + # 检查源目录结构:可能是 frontend/dist/assets/ 或 frontend/dist/ 直接包含文件 + assets_source = frontend_dist_source / "assets" + if assets_source.exists() and assets_source.is_dir(): + # 如果源目录有 assets 子目录,复制 assets 目录内容 + source_dir = assets_source + else: + # 否则直接复制 dist 目录下的文件 + source_dir = frontend_dist_source + + # 复制所有构建文件 + for item in source_dir.iterdir(): + if item.is_file(): + # 复制文件到工具独立目录 + shutil.copy2(item, tool_assets_dir / item.name) + logger.info(f"复制构建文件: {item} -> {tool_assets_dir / item.name}") + elif item.is_dir(): + # 如果是目录,递归复制 + target_dir = tool_assets_dir / item.name + if target_dir.exists(): + shutil.rmtree(target_dir) + shutil.copytree(item, target_dir) + logger.info(f"复制构建目录: {item} -> {target_dir}") + + # 复制 manifest.json(如果存在) + manifest_source = frontend_dist_source / "manifest.json" + if manifest_source.exists(): + shutil.copy2(manifest_source, tool_dist_dir / "manifest.json") + logger.info(f"复制 manifest 文件: {manifest_source} -> {tool_dist_dir / 'manifest.json'}") + + logger.info(f"生产环境:已复制工具构建文件到独立目录 dist/tools/{tool_name}/assets/") + else: + # 生产环境但没有构建文件,尝试复制源码(可能需要重新构建) + frontend_source = tool_dir_path / "frontend" / tool_name + if frontend_source.exists() and frontend_source.is_dir(): + tool_frontend_dir = frontend_root / "src" / "views" / "tools" / tool_name + if tool_frontend_dir.exists(): + shutil.rmtree(tool_frontend_dir) + shutil.copytree(frontend_source, tool_frontend_dir) + logger.warning(f"生产环境:工具包未包含构建文件,已复制源码到 {tool_frontend_dir},需要重新构建前端") + else: + logger.warning(f"生产环境:工具包未找到前端文件(既无构建文件也无源码)") + else: + # 开发环境:复制源码 + frontend_source = tool_dir_path / "frontend" / tool_name + if frontend_source.exists() and frontend_source.is_dir(): + tool_frontend_dir = frontend_root / "src" / "views" / "tools" / tool_name + if tool_frontend_dir.exists(): + shutil.rmtree(tool_frontend_dir) + shutil.copytree(frontend_source, tool_frontend_dir) + logger.info(f"开发环境:复制前端组件目录: {frontend_source} -> {tool_frontend_dir}") + return { 'success': True, 'tool_name': tool_name, 'tool_title': tool_data.get('title', tool_name), - 'message': f"工具 {tool_name} 安装成功" + 'message': f"工具 {tool_name} 安装成功", + 'environment': 'production' if is_production else 'development' } except Exception as e: @@ -316,10 +379,11 @@ async def uninstall_tool(tool_name: str): try: logger.info(f"开始卸载工具: {tool_name}") jingrow_root = get_jingrow_root() + frontend_root = jingrow_root.parent / "frontend" - # 前端目录:apps/jingrow/frontend/src/views/tools/{tool_name} - frontend_root = jingrow_root.parent / "frontend" / "src" - tool_frontend_dir = frontend_root / "views" / "tools" / tool_name + # 前端源码目录:apps/jingrow/frontend/src/views/tools/{tool_name} + frontend_src_root = frontend_root / "src" + tool_frontend_dir = frontend_src_root / "views" / "tools" / tool_name # 后端目录:apps/jingrow/jingrow/tools/{tool_name} tool_backend_dir = jingrow_root / "tools" / tool_name @@ -330,15 +394,26 @@ async def uninstall_tool(tool_name: str): logger.info(f"后端目录存在: {tool_backend_dir.exists()}") deleted_paths = [] + warnings = [] - # 删除前端目录 + # 删除前端源码目录 if tool_frontend_dir.exists(): shutil.rmtree(tool_frontend_dir) - deleted_paths.append(f"前端: {tool_frontend_dir}") + deleted_paths.append(f"前端源码: {tool_frontend_dir}") logger.info(f"删除工具前端目录成功: {tool_frontend_dir}") else: logger.warning(f"前端目录不存在: {tool_frontend_dir}") + # 生产环境:删除工具的独立构建目录 + if _is_production_environment(): + tool_dist_dir = frontend_root / "dist" / "tools" / tool_name + if tool_dist_dir.exists(): + shutil.rmtree(tool_dist_dir) + deleted_paths.append(f"前端构建文件: {tool_dist_dir}") + logger.info(f"删除工具构建目录成功: {tool_dist_dir}") + else: + logger.info(f"工具构建目录不存在: {tool_dist_dir}") + # 删除后端目录 if tool_backend_dir.exists(): shutil.rmtree(tool_backend_dir) @@ -356,11 +431,16 @@ async def uninstall_tool(tool_name: str): 'backend_path': str(tool_backend_dir) } + message = f'工具 {tool_name} 卸载成功' + if warnings: + message += f"。注意:{warnings[0]}" + logger.info(f"工具 {tool_name} 卸载成功,已删除路径: {deleted_paths}") return { 'success': True, - 'message': f'工具 {tool_name} 卸载成功', - 'deleted_paths': deleted_paths + 'message': message, + 'deleted_paths': deleted_paths, + 'warnings': warnings } except Exception as e: @@ -532,3 +612,95 @@ async def delete_published_tool(request: Request, payload: Dict[str, Any]): logger.error(f"删除已发布工具失败: {str(e)}") raise HTTPException(status_code=500, detail=f"删除已发布工具失败: {str(e)}") + +@router.post("/jingrow/build-tool/{tool_name}") +async def build_tool(tool_name: str): + """单独构建工具 + + 构建工具的前端组件,生成生产环境可用的构建文件。 + 构建输出位于: apps/jingrow/frontend/dist/tools/{tool_name}/ + """ + try: + jingrow_root = get_jingrow_root() + frontend_root = jingrow_root.parent / "frontend" + tool_dir = frontend_root / "src" / "views" / "tools" / tool_name + tool_vue_file = tool_dir / f"{tool_name}.vue" + + # 检查工具是否存在 + if not tool_vue_file.exists(): + raise HTTPException( + status_code=404, + detail=f"工具 {tool_name} 不存在,路径: {tool_vue_file}" + ) + + # 检查 Node.js 和 npm 是否可用 + try: + subprocess.run(['node', '--version'], capture_output=True, check=True, timeout=5) + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + raise HTTPException( + status_code=500, + detail="Node.js 未安装或不可用,无法构建工具" + ) + + # 切换到前端目录 + original_cwd = os.getcwd() + os.chdir(str(frontend_root)) + + try: + # 执行构建脚本 + build_script = frontend_root / "build-tool.js" + if not build_script.exists(): + raise HTTPException( + status_code=500, + detail=f"构建脚本不存在: {build_script}" + ) + + logger.info(f"开始构建工具: {tool_name}") + result = subprocess.run( + ['node', 'build-tool.js', tool_name], + capture_output=True, + text=True, + timeout=300, # 5分钟超时 + cwd=str(frontend_root) + ) + + if result.returncode != 0: + error_msg = result.stderr or result.stdout + logger.error(f"构建工具失败: {error_msg}") + raise HTTPException( + status_code=500, + detail=f"构建工具失败: {error_msg}" + ) + + # 读取 manifest 文件 + output_dir = frontend_root / "dist" / "tools" / tool_name + manifest_file = output_dir / "manifest.json" + + manifest_data = {} + if manifest_file.exists(): + with open(manifest_file, 'r', encoding='utf-8') as f: + manifest_data = json.load(f) + + logger.info(f"工具 {tool_name} 构建成功") + + return { + 'success': True, + 'tool_name': tool_name, + 'output_dir': str(output_dir), + 'manifest': manifest_data, + 'message': f"工具 {tool_name} 构建成功", + 'build_output': result.stdout + } + + finally: + os.chdir(original_cwd) + + except HTTPException: + raise + except subprocess.TimeoutExpired: + raise HTTPException(status_code=500, detail="构建超时(超过5分钟)") + except Exception as e: + logger.error(f"构建工具失败: {str(e)}") + logger.error(f"Traceback: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=f"构建工具失败: {str(e)}") +