auto-generate routeName and routePath from tool_name
This commit is contained in:
parent
984d23723e
commit
9170ae0ad2
@ -136,7 +136,8 @@ export const useToolsStore = defineStore('tools', () => {
|
|||||||
saveUserTools(userTools.value)
|
saveUserTools(userTools.value)
|
||||||
|
|
||||||
// 如果是路由类型工具,注册路由(如果提供了 router)
|
// 如果是路由类型工具,注册路由(如果提供了 router)
|
||||||
if (tool.type === 'route' && tool.routeName && router) {
|
// routeName 会在 registerToolRoute 中自动生成
|
||||||
|
if (tool.type === 'route' && router) {
|
||||||
registerToolRoute(router, tool, componentPath)
|
registerToolRoute(router, tool, componentPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,48 @@
|
|||||||
import type { Router, RouteRecordRaw } from 'vue-router'
|
import type { Router, RouteRecordRaw } from 'vue-router'
|
||||||
import type { Tool } from '../stores/tools'
|
import type { Tool } from '../stores/tools'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 snake_case 转换为 PascalCase
|
||||||
|
*/
|
||||||
|
function snakeToPascal(snakeStr: string): string {
|
||||||
|
const components = snakeStr.split('_')
|
||||||
|
return components.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 tool_name 生成 routeName(PascalCase)
|
||||||
|
*/
|
||||||
|
function generateRouteName(toolName: string): string {
|
||||||
|
return snakeToPascal(toolName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 tool_name 生成 routePath(约定:tools/{tool_name})
|
||||||
|
*/
|
||||||
|
function generateRoutePath(toolName: string): string {
|
||||||
|
return `tools/${toolName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保工具具有 routeName 和 routePath(如果缺失则自动生成)
|
||||||
|
*/
|
||||||
|
function ensureToolRoutes(tool: Tool): Tool {
|
||||||
|
if (tool.type === 'route') {
|
||||||
|
// 如果没有 routeName,基于 tool.id 或 tool.name 生成
|
||||||
|
if (!tool.routeName) {
|
||||||
|
const baseName = tool.id || tool.name
|
||||||
|
tool.routeName = generateRouteName(baseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有 routePath,基于 tool.id 或 tool.name 生成
|
||||||
|
if (!tool.routePath) {
|
||||||
|
const baseName = tool.id || tool.name
|
||||||
|
tool.routePath = generateRoutePath(baseName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tool
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态注册工具路由
|
* 动态注册工具路由
|
||||||
* @param router Vue Router 实例
|
* @param router Vue Router 实例
|
||||||
@ -17,24 +59,29 @@ export function registerToolRoute(
|
|||||||
tool: Tool,
|
tool: Tool,
|
||||||
componentPath?: string
|
componentPath?: string
|
||||||
): boolean {
|
): boolean {
|
||||||
if (tool.type !== 'route' || !tool.routeName) {
|
// 确保工具具有 routeName 和 routePath
|
||||||
|
const toolWithRoutes = ensureToolRoutes({ ...tool })
|
||||||
|
|
||||||
|
if (toolWithRoutes.type !== 'route' || !toolWithRoutes.routeName) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查路由是否已存在
|
// 检查路由是否已存在
|
||||||
if (router.hasRoute(tool.routeName)) {
|
if (router.hasRoute(toolWithRoutes.routeName)) {
|
||||||
console.warn(`Route ${tool.routeName} already exists, skipping registration`)
|
console.warn(`Route ${toolWithRoutes.routeName} already exists, skipping registration`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定组件路径和路由路径
|
// 确定组件路径和路由路径
|
||||||
const finalComponentPath = componentPath || tool.componentPath || `../../views/tools/${tool.routeName}.vue`
|
// 默认路径:tools/{tool_id}/{tool_id}.vue(每个工具独立文件夹,入口文件与工具ID一致)
|
||||||
const routePath = tool.routePath || `tools/${tool.id}`
|
const defaultComponentPath = toolWithRoutes.componentPath || `../../views/tools/${toolWithRoutes.id}/${toolWithRoutes.id}.vue`
|
||||||
|
const finalComponentPath = componentPath || defaultComponentPath
|
||||||
|
const routePath = toolWithRoutes.routePath || `tools/${toolWithRoutes.id}`
|
||||||
|
|
||||||
// 创建路由配置,添加组件加载错误处理
|
// 创建路由配置,添加组件加载错误处理
|
||||||
const route: RouteRecordRaw = {
|
const route: RouteRecordRaw = {
|
||||||
path: routePath,
|
path: routePath,
|
||||||
name: tool.routeName,
|
name: toolWithRoutes.routeName,
|
||||||
component: () => import(finalComponentPath).catch((error) => {
|
component: () => import(finalComponentPath).catch((error) => {
|
||||||
console.error(`Failed to load tool component: ${finalComponentPath}`, error)
|
console.error(`Failed to load tool component: ${finalComponentPath}`, error)
|
||||||
// 返回一个简单的错误组件
|
// 返回一个简单的错误组件
|
||||||
@ -45,8 +92,8 @@ export function registerToolRoute(
|
|||||||
}),
|
}),
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
toolId: tool.id,
|
toolId: toolWithRoutes.id,
|
||||||
toolName: tool.name
|
toolName: toolWithRoutes.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,10 +101,10 @@ export function registerToolRoute(
|
|||||||
// 将路由添加到 AppLayout 的 children 下
|
// 将路由添加到 AppLayout 的 children 下
|
||||||
// AppLayout 路由应该在应用启动时已存在
|
// AppLayout 路由应该在应用启动时已存在
|
||||||
router.addRoute('AppLayout', route)
|
router.addRoute('AppLayout', route)
|
||||||
console.log(`Tool route registered: ${tool.routeName} -> ${routePath}`)
|
console.log(`Tool route registered: ${toolWithRoutes.routeName} -> ${routePath}`)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register tool route ${tool.routeName}:`, error)
|
console.error(`Failed to register tool route ${toolWithRoutes.routeName}:`, error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,7 +135,8 @@ export function unregisterToolRoute(router: Router, routeName: string): boolean
|
|||||||
* @param tools 工具列表
|
* @param tools 工具列表
|
||||||
*/
|
*/
|
||||||
export function registerAllToolRoutes(router: Router, tools: Tool[]): void {
|
export function registerAllToolRoutes(router: Router, tools: Tool[]): void {
|
||||||
const routeTools = tools.filter(t => t.type === 'route' && t.routeName && !t.isDefault)
|
// 过滤出路由类型的工具(routeName 会在 registerToolRoute 中自动生成)
|
||||||
|
const routeTools = tools.filter(t => t.type === 'route' && !t.isDefault)
|
||||||
|
|
||||||
routeTools.forEach(tool => {
|
routeTools.forEach(tool => {
|
||||||
registerToolRoute(router, tool)
|
registerToolRoute(router, tool)
|
||||||
|
|||||||
212
apps/jingrow/frontend/src/views/tools/TestTool.vue
Normal file
212
apps/jingrow/frontend/src/views/tools/TestTool.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div class="test-tool-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>{{ t('Test Tool') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="test-card">
|
||||||
|
<div class="test-icon">
|
||||||
|
<i class="fa fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<h3>{{ t('Dynamic Route Registration Test') }}</h3>
|
||||||
|
<p>{{ t('This tool is installed dynamically and its route is registered automatically') }}</p>
|
||||||
|
|
||||||
|
<div class="test-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>{{ t('Route Name') }}:</strong>
|
||||||
|
<span>TestTool</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>{{ t('Route Path') }}:</strong>
|
||||||
|
<span>/tools/test-tool</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>{{ t('Component Path') }}:</strong>
|
||||||
|
<span>views/tools/TestTool.vue</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-actions">
|
||||||
|
<button class="test-btn" @click="handleTest">
|
||||||
|
<i class="fa fa-flask"></i>
|
||||||
|
{{ t('Run Test') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="testResult" class="test-result">
|
||||||
|
<div :class="['result-message', testResult.success ? 'success' : 'error']">
|
||||||
|
<i :class="testResult.success ? 'fa fa-check-circle' : 'fa fa-times-circle'"></i>
|
||||||
|
<span>{{ testResult.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { t } from '../../shared/i18n'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const testResult = ref<{ success: boolean; message: string } | null>(null)
|
||||||
|
|
||||||
|
function handleTest() {
|
||||||
|
const currentRoute = router.currentRoute.value
|
||||||
|
testResult.value = {
|
||||||
|
success: true,
|
||||||
|
message: t('Route registration test passed! Current route: ') + currentRoute.name
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
testResult.value = null
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-tool-page {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
background: #e6f8f0;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-icon i {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #1fc76f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-card h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-card p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0 0 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-info {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item strong {
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item span {
|
||||||
|
color: #1f2937;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-actions {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn {
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1fc76f;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn:hover {
|
||||||
|
background: #1dd87f;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(31, 199, 111, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message.success {
|
||||||
|
background: #e6f8f0;
|
||||||
|
color: #0d684b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -6,18 +6,41 @@ Jingrow Tools API
|
|||||||
工具相关的 FastAPI 路由
|
工具相关的 FastAPI 路由
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Form, UploadFile, File
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
import subprocess
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers
|
from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers
|
||||||
|
from jingrow.utils.path import get_root_path, get_jingrow_root
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def snake_to_pascal(snake_str: str) -> str:
|
||||||
|
"""将 snake_case 转换为 PascalCase"""
|
||||||
|
components = snake_str.split('_')
|
||||||
|
return ''.join(word.capitalize() for word in components)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_route_name(tool_name: str) -> str:
|
||||||
|
"""基于 tool_name 生成 routeName(PascalCase)"""
|
||||||
|
return snake_to_pascal(tool_name)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_route_path(tool_name: str) -> str:
|
||||||
|
"""基于 tool_name 生成 routePath(约定:tools/{tool_name})"""
|
||||||
|
return f"tools/{tool_name}"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/jingrow/tool-marketplace")
|
@router.get("/jingrow/tool-marketplace")
|
||||||
async def get_tool_marketplace(
|
async def get_tool_marketplace(
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
@ -104,3 +127,218 @@ async def get_tool_detail(name: str):
|
|||||||
logger.error(f"获取工具详情失败: {e}", exc_info=True)
|
logger.error(f"获取工具详情失败: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=f"获取工具详情失败: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"获取工具详情失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/jingrow/install-tool-from-file")
|
||||||
|
async def install_tool_from_file(file: UploadFile = File(...)):
|
||||||
|
"""从上传的文件安装工具(支持ZIP,每个工具包独立)"""
|
||||||
|
try:
|
||||||
|
root = get_root_path()
|
||||||
|
tmp_dir = root / "tmp"
|
||||||
|
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 创建临时文件
|
||||||
|
temp_filename = f"tool_upload_{uuid.uuid4().hex[:8]}.zip"
|
||||||
|
temp_file_path = tmp_dir / temp_filename
|
||||||
|
|
||||||
|
# 保存上传的文件
|
||||||
|
with open(temp_file_path, 'wb') as f:
|
||||||
|
shutil.copyfileobj(file.file, f)
|
||||||
|
|
||||||
|
# 安装工具
|
||||||
|
result = _install_tool_from_file(str(temp_file_path))
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_file_path.exists():
|
||||||
|
os.remove(temp_file_path)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/jingrow/install-tool-from-url")
|
||||||
|
async def install_tool_from_url(url: str = Form(...)):
|
||||||
|
"""从URL安装工具"""
|
||||||
|
try:
|
||||||
|
root = get_root_path()
|
||||||
|
tmp_dir = root / "tmp"
|
||||||
|
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 创建临时文件
|
||||||
|
temp_filename = f"tool_download_{uuid.uuid4().hex[:8]}{Path(url).suffix or '.zip'}"
|
||||||
|
temp_file_path = tmp_dir / temp_filename
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
response = requests.get(url, stream=True, timeout=300)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with open(temp_file_path, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
# 安装工具
|
||||||
|
result = _install_tool_from_file(str(temp_file_path))
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_file_path.exists():
|
||||||
|
os.remove(temp_file_path)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从URL安装工具失败: {str(e)}")
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"安装工具失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_tool_from_file(file_path: str) -> Dict[str, Any]:
|
||||||
|
"""从文件安装工具(支持ZIP,每个工具包独立)"""
|
||||||
|
try:
|
||||||
|
from jingrow.utils.app_installer import extract_package, cleanup_temp_dir
|
||||||
|
|
||||||
|
# 解压文件
|
||||||
|
extract_result = extract_package(file_path)
|
||||||
|
if not extract_result.get('success'):
|
||||||
|
return extract_result
|
||||||
|
|
||||||
|
temp_dir = extract_result['temp_dir']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 查找工具定义文件 {tool_id}.json
|
||||||
|
# 工具包结构应该是:
|
||||||
|
# - {tool_id}.json(必需,在根目录,文件名与工具ID一致)
|
||||||
|
# - frontend/{tool_id}/{tool_id}.vue(前端组件)
|
||||||
|
# - backend/{tool_id}/{tool_id}.py(后端文件,可选)
|
||||||
|
|
||||||
|
tool_json_path = None
|
||||||
|
|
||||||
|
# 查找根目录下的 {tool_id}.json 文件
|
||||||
|
# 遍历根目录,找到第一个 .json 文件(应该是工具ID命名的)
|
||||||
|
json_files = [f for f in Path(temp_dir).iterdir()
|
||||||
|
if f.is_file() and f.suffix == '.json' and not f.name.startswith('.')]
|
||||||
|
|
||||||
|
if json_files:
|
||||||
|
# 使用第一个找到的 JSON 文件
|
||||||
|
tool_json_path = json_files[0]
|
||||||
|
else:
|
||||||
|
return {'success': False, 'error': '压缩包中没有找到工具定义 JSON 文件(应为 {tool_id}.json)'}
|
||||||
|
|
||||||
|
# 安装工具(每个包只包含一个工具)
|
||||||
|
tool_dir = tool_json_path.parent
|
||||||
|
result = _install_single_tool_directory(str(tool_dir))
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'tool_id': result.get('tool_id'),
|
||||||
|
'tool_name': result.get('tool_name'),
|
||||||
|
'message': f"工具 {result.get('tool_name')} 安装成功"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cleanup_temp_dir(temp_dir)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从文件安装工具失败: {str(e)}")
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _install_single_tool_directory(tool_dir: str) -> Dict[str, Any]:
|
||||||
|
"""安装单个工具目录(每个工具独立)"""
|
||||||
|
try:
|
||||||
|
tool_dir_path = Path(tool_dir)
|
||||||
|
|
||||||
|
# 读取工具定义文件 {tool_name}.json
|
||||||
|
# 查找 {tool_name}.json 文件
|
||||||
|
json_files = [f for f in tool_dir_path.iterdir()
|
||||||
|
if f.is_file() and f.suffix == '.json' and not f.name.startswith('.')]
|
||||||
|
if not json_files:
|
||||||
|
return {'success': False, 'error': '找不到工具定义文件: 应为 {tool_name}.json'}
|
||||||
|
|
||||||
|
tool_json = json_files[0]
|
||||||
|
|
||||||
|
with open(tool_json, 'r', encoding='utf-8') as f:
|
||||||
|
tool_data = json.load(f)
|
||||||
|
|
||||||
|
if not isinstance(tool_data, dict):
|
||||||
|
return {'success': False, 'error': '工具定义文件格式错误'}
|
||||||
|
|
||||||
|
tool_name = tool_data.get('tool_name')
|
||||||
|
if not tool_name:
|
||||||
|
return {'success': False, 'error': '工具定义中缺少 tool_name'}
|
||||||
|
|
||||||
|
# 验证 JSON 文件名必须与工具名称一致
|
||||||
|
json_filename = tool_json.stem # 获取文件名(不含扩展名)
|
||||||
|
if json_filename != tool_name:
|
||||||
|
return {'success': False, 'error': f'工具定义文件名 {json_filename}.json 与工具名称 {tool_name} 不一致,必须使用 {tool_name}.json'}
|
||||||
|
|
||||||
|
# 自动生成 routeName 和 routePath(如果未提供)
|
||||||
|
if tool_data.get('type') == 'route':
|
||||||
|
if not tool_data.get('routeName'):
|
||||||
|
tool_data['routeName'] = generate_route_name(tool_name)
|
||||||
|
if not tool_data.get('routePath'):
|
||||||
|
tool_data['routePath'] = generate_route_path(tool_name)
|
||||||
|
|
||||||
|
# 将更新后的数据写回 JSON 文件
|
||||||
|
with open(tool_json, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(tool_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 确定目标目录: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)
|
||||||
|
|
||||||
|
# 复制前端组件文件(如果存在)
|
||||||
|
frontend_source = tool_dir_path / "frontend"
|
||||||
|
if frontend_source.exists() and frontend_source.is_dir():
|
||||||
|
# 如果前端目录下有子目录(如 frontend/{tool_id}/),复制整个子目录
|
||||||
|
# 否则直接复制 frontend/ 下的所有文件到工具目录
|
||||||
|
subdirs = [d for d in frontend_source.iterdir() if d.is_dir()]
|
||||||
|
if subdirs:
|
||||||
|
# 有子目录,复制第一个子目录的内容
|
||||||
|
source_subdir = subdirs[0]
|
||||||
|
if tool_frontend_dir.exists():
|
||||||
|
shutil.rmtree(tool_frontend_dir)
|
||||||
|
shutil.copytree(source_subdir, tool_frontend_dir)
|
||||||
|
logger.info(f"复制前端组件目录: {source_subdir} -> {tool_frontend_dir}")
|
||||||
|
else:
|
||||||
|
# 没有子目录,直接复制所有文件
|
||||||
|
if tool_frontend_dir.exists():
|
||||||
|
shutil.rmtree(tool_frontend_dir)
|
||||||
|
tool_frontend_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for item in frontend_source.iterdir():
|
||||||
|
if item.is_file():
|
||||||
|
shutil.copy2(item, tool_frontend_dir / item.name)
|
||||||
|
logger.info(f"复制前端组件: {item.name} -> {tool_frontend_dir}")
|
||||||
|
elif item.is_dir():
|
||||||
|
shutil.copytree(item, tool_frontend_dir / item.name)
|
||||||
|
logger.info(f"复制前端组件目录: {item.name} -> {tool_frontend_dir}")
|
||||||
|
|
||||||
|
# 复制后端文件(如果存在)
|
||||||
|
backend_source = tool_dir_path / "backend"
|
||||||
|
if backend_source.exists() and backend_source.is_dir():
|
||||||
|
backend_target = jingrow_root / "tools" / tool_name
|
||||||
|
backend_target.mkdir(parents=True, exist_ok=True)
|
||||||
|
# 如果目录已存在,先删除
|
||||||
|
if backend_target.exists():
|
||||||
|
shutil.rmtree(backend_target)
|
||||||
|
shutil.copytree(backend_source, backend_target)
|
||||||
|
logger.info(f"复制后端文件: {backend_source} -> {backend_target}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'tool_name': tool_name,
|
||||||
|
'tool_title': tool_data.get('title', tool_name),
|
||||||
|
'message': f"工具 {tool_name} 安装成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"安装工具目录失败: {str(e)}")
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user