replace JSON file metadata reading with database queries
This commit is contained in:
parent
c01bdf9a41
commit
4046072f27
@ -255,6 +255,7 @@ async function performInstall() {
|
||||
installMessage.value = t('正在安装工具...')
|
||||
|
||||
response = await axios.post('/jingrow/install-tool-from-url', new URLSearchParams({
|
||||
tool_name: tool.value.name || tool.value.tool_name,
|
||||
url: tool.value.file_url
|
||||
}), {
|
||||
headers: {
|
||||
|
||||
@ -275,7 +275,8 @@ async function performInstall(tool: any) {
|
||||
installMessage.value = t('正在安装工具...')
|
||||
|
||||
response = await axios.post('/jingrow/install-tool-from-url', new URLSearchParams({
|
||||
url: tool.file_url
|
||||
url: tool.file_url,
|
||||
tool_name: tool.name || tool.tool_name
|
||||
}), {
|
||||
headers: {
|
||||
...get_session_api_headers(),
|
||||
|
||||
233
apps/jingrow/frontend/src/views/tools/test_tool/test_tool.vue
Normal file
233
apps/jingrow/frontend/src/views/tools/test_tool/test_tool.vue
Normal file
@ -0,0 +1,233 @@
|
||||
<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>{{ routeName }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>{{ t('Route Path') }}:</strong>
|
||||
<span>{{ routePath }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>{{ t('Component Path') }}:</strong>
|
||||
<span>{{ componentPath }}</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, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { useToolsStore } from '@/shared/stores/tools'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const toolsStore = useToolsStore()
|
||||
const testResult = ref<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
// 获取当前工具信息
|
||||
const currentTool = computed(() => {
|
||||
return toolsStore.userTools.find(tool => tool.routeName === route.name)
|
||||
})
|
||||
|
||||
// 计算组件路径
|
||||
const componentPath = computed(() => {
|
||||
if (currentTool.value?.componentPath) {
|
||||
return `views/${currentTool.value.componentPath}`
|
||||
}
|
||||
// 如果没有 componentPath,从 route path 推断
|
||||
const toolName = route.path.split('/').pop() || 'test_tool'
|
||||
return `views/tools/${toolName}/${toolName}.vue`
|
||||
})
|
||||
|
||||
const routeName = computed(() => route.name || 'TestTool')
|
||||
const routePath = computed(() => route.path || '/tools/test_tool')
|
||||
|
||||
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>
|
||||
@ -129,9 +129,14 @@ async def get_tool_detail(name: str):
|
||||
|
||||
|
||||
@router.post("/jingrow/install-tool-from-file")
|
||||
async def install_tool_from_file(file: UploadFile = File(...)):
|
||||
async def install_tool_from_file(file: UploadFile = File(...), tool_name: str = Form(...)):
|
||||
"""从上传的文件安装工具(支持ZIP,每个工具包独立)"""
|
||||
try:
|
||||
# 从数据库获取工具元数据
|
||||
tool_data = await get_tool_detail(tool_name)
|
||||
if not tool_data:
|
||||
raise HTTPException(status_code=404, detail=f"工具 {tool_name} 不存在")
|
||||
|
||||
root = get_root_path()
|
||||
tmp_dir = root / "tmp"
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
@ -144,8 +149,8 @@ async def install_tool_from_file(file: UploadFile = File(...)):
|
||||
with open(temp_file_path, 'wb') as f:
|
||||
shutil.copyfileobj(file.file, f)
|
||||
|
||||
# 安装工具
|
||||
result = _install_tool_from_file(str(temp_file_path))
|
||||
# 安装工具(传递工具元数据)
|
||||
result = _install_tool_from_file(str(temp_file_path), tool_data)
|
||||
|
||||
# 清理临时文件
|
||||
if temp_file_path.exists():
|
||||
@ -153,6 +158,8 @@ async def install_tool_from_file(file: UploadFile = File(...)):
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"从文件安装工具失败: {str(e)}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
@ -160,9 +167,14 @@ async def install_tool_from_file(file: UploadFile = File(...)):
|
||||
|
||||
|
||||
@router.post("/jingrow/install-tool-from-url")
|
||||
async def install_tool_from_url(url: str = Form(...)):
|
||||
async def install_tool_from_url(url: str = Form(...), tool_name: str = Form(...)):
|
||||
"""从URL安装工具"""
|
||||
try:
|
||||
# 从数据库获取工具元数据
|
||||
tool_data = await get_tool_detail(tool_name)
|
||||
if not tool_data:
|
||||
raise HTTPException(status_code=404, detail=f"工具 {tool_name} 不存在")
|
||||
|
||||
root = get_root_path()
|
||||
tmp_dir = root / "tmp"
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
@ -179,8 +191,8 @@ async def install_tool_from_url(url: str = Form(...)):
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
# 安装工具
|
||||
result = _install_tool_from_file(str(temp_file_path))
|
||||
# 安装工具(传递工具元数据)
|
||||
result = _install_tool_from_file(str(temp_file_path), tool_data)
|
||||
|
||||
# 清理临时文件
|
||||
if temp_file_path.exists():
|
||||
@ -188,13 +200,15 @@ async def install_tool_from_url(url: str = Form(...)):
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
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]:
|
||||
def _install_tool_from_file(file_path: str, tool_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""从文件安装工具(支持ZIP,每个工具包独立)"""
|
||||
try:
|
||||
from jingrow.utils.app_installer import extract_package, cleanup_temp_dir
|
||||
@ -207,28 +221,9 @@ def _install_tool_from_file(file_path: str) -> Dict[str, Any]:
|
||||
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))
|
||||
# 安装工具(传递工具元数据)
|
||||
tool_dir = Path(temp_dir)
|
||||
result = _install_single_tool_directory(str(tool_dir), tool_data)
|
||||
|
||||
if result.get('success'):
|
||||
return {
|
||||
@ -249,45 +244,15 @@ def _install_tool_from_file(file_path: str) -> Dict[str, Any]:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
def _install_single_tool_directory(tool_dir: str) -> Dict[str, Any]:
|
||||
def _install_single_tool_directory(tool_dir: str, tool_data: Dict[str, Any]) -> 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')
|
||||
# 从数据库获取的元数据中提取 tool_name
|
||||
tool_name = tool_data.get('tool_name') or tool_data.get('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)
|
||||
return {'success': False, 'error': '工具元数据中缺少 tool_name'}
|
||||
|
||||
# 确定目标目录:apps/jingrow/frontend/src/views/tools/{tool_name}
|
||||
jingrow_root = get_jingrow_root()
|
||||
|
||||
18
apps/jingrow/jingrow/tools/test_tool/test_tool.py
Normal file
18
apps/jingrow/jingrow/tools/test_tool/test_tool.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Test Tool Backend
|
||||
# 这是一个示例后端文件,展示如何为工具添加后端功能
|
||||
|
||||
"""
|
||||
Test Tool 后端模块
|
||||
|
||||
如果工具需要后端 API 支持,可以在这里添加相应的处理逻辑。
|
||||
后端文件会被安装到 apps/jingrow/jingrow/tools/test_tool/
|
||||
"""
|
||||
|
||||
def get_tool_info():
|
||||
"""获取工具信息"""
|
||||
return {
|
||||
"id": "test_tool",
|
||||
"name": "Test Tool",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user