auto-generate routeName and routePath from tool_name

This commit is contained in:
jingrow 2025-11-21 15:44:47 +08:00
parent 984d23723e
commit 809aebee55
3 changed files with 300 additions and 13 deletions

View File

@ -136,7 +136,8 @@ export const useToolsStore = defineStore('tools', () => {
saveUserTools(userTools.value)
// 如果是路由类型工具,注册路由(如果提供了 router
if (tool.type === 'route' && tool.routeName && router) {
// routeName 会在 registerToolRoute 中自动生成
if (tool.type === 'route' && router) {
registerToolRoute(router, tool, componentPath)
}
}

View File

@ -6,6 +6,48 @@
import type { Router, RouteRecordRaw } from 'vue-router'
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 routeNamePascalCase
*/
function generateRouteName(toolName: string): string {
return snakeToPascal(toolName)
}
/**
* tool_name routePathtools/{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
@ -17,24 +59,29 @@ export function registerToolRoute(
tool: Tool,
componentPath?: string
): boolean {
if (tool.type !== 'route' || !tool.routeName) {
// 确保工具具有 routeName 和 routePath
const toolWithRoutes = ensureToolRoutes({ ...tool })
if (toolWithRoutes.type !== 'route' || !toolWithRoutes.routeName) {
return false
}
// 检查路由是否已存在
if (router.hasRoute(tool.routeName)) {
console.warn(`Route ${tool.routeName} already exists, skipping registration`)
if (router.hasRoute(toolWithRoutes.routeName)) {
console.warn(`Route ${toolWithRoutes.routeName} already exists, skipping registration`)
return false
}
// 确定组件路径和路由路径
const finalComponentPath = componentPath || tool.componentPath || `../../views/tools/${tool.routeName}.vue`
const routePath = tool.routePath || `tools/${tool.id}`
// 默认路径tools/{tool_id}/{tool_id}.vue每个工具独立文件夹入口文件与工具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 = {
path: routePath,
name: tool.routeName,
name: toolWithRoutes.routeName,
component: () => import(finalComponentPath).catch((error) => {
console.error(`Failed to load tool component: ${finalComponentPath}`, error)
// 返回一个简单的错误组件
@ -45,8 +92,8 @@ export function registerToolRoute(
}),
meta: {
requiresAuth: true,
toolId: tool.id,
toolName: tool.name
toolId: toolWithRoutes.id,
toolName: toolWithRoutes.name
}
}
@ -54,10 +101,10 @@ export function registerToolRoute(
// 将路由添加到 AppLayout 的 children 下
// AppLayout 路由应该在应用启动时已存在
router.addRoute('AppLayout', route)
console.log(`Tool route registered: ${tool.routeName} -> ${routePath}`)
console.log(`Tool route registered: ${toolWithRoutes.routeName} -> ${routePath}`)
return true
} catch (error) {
console.error(`Failed to register tool route ${tool.routeName}:`, error)
console.error(`Failed to register tool route ${toolWithRoutes.routeName}:`, error)
return false
}
}
@ -88,7 +135,8 @@ export function unregisterToolRoute(router: Router, routeName: string): boolean
* @param tools
*/
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 => {
registerToolRoute(router, tool)

View File

@ -6,18 +6,41 @@ Jingrow Tools API
工具相关的 FastAPI 路由
"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Form, UploadFile, File
from typing import Dict, Any, Optional
import json
import requests
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.path import get_root_path, get_jingrow_root
logger = logging.getLogger(__name__)
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 生成 routeNamePascalCase"""
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")
async def get_tool_marketplace(
search: Optional[str] = None,
@ -104,3 +127,218 @@ async def get_tool_detail(name: str):
logger.error(f"获取工具详情失败: {e}", exc_info=True)
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)}