add standalone tool build and production dynamic loading
This commit is contained in:
parent
85058f51e7
commit
22dec06133
208
apps/jingrow/frontend/build-tool.js
Normal file
208
apps/jingrow/frontend/build-tool.js
Normal file
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 单独构建工具脚本
|
||||
* 用法: node build-tool.js <tool_name>
|
||||
* 示例: 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 <tool_name>')
|
||||
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 = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${toolName}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./${toolName}.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:tool": "node build-tool.js",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -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<any> {
|
||||
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(
|
||||
<div style="padding: 20px; text-align: center;">
|
||||
<h3>Component Not Found</h3>
|
||||
<p>Failed to load component: ${finalComponentPath}</p>
|
||||
<p style="color: #666; font-size: 12px; margin-top: 10px;">Error: ${error.message || error}</p>
|
||||
<p style="color: #666; font-size: 12px; margin-top: 10px;">Error: ${errorMessage}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
|
||||
@ -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)}")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user