修复应用安装界面无法通过上传安装包安装app的问题

This commit is contained in:
jingrow 2025-10-26 22:05:42 +08:00
parent 51d9f9b0a1
commit 916ec1b69c
3 changed files with 189 additions and 118 deletions

View File

@ -223,15 +223,9 @@ const beforeUpload = (data: { file: UploadFileInfo }) => {
const handleFileChange = (options: { fileList: UploadFileInfo[] }) => {
fileList.value = options.fileList
// 使
if (fileList.value.length > 0) {
const fileName = fileList.value[0].file?.name || ''
isExtensionFile.value = fileName.toLowerCase().endsWith('.tar.gz') ||
fileName.toLowerCase().endsWith('.tgz') ||
fileName.toLowerCase().endsWith('.gz')
if (isExtensionFile.value) {
appName.value = ''
}
isExtensionFile.value = false
}
}
@ -251,77 +245,7 @@ const startUpload = async () => {
return
}
const fileName = file.name.toLowerCase()
const isExtensionPackage = fileName.endsWith('.tar.gz') || fileName.endsWith('.tgz') || fileName.endsWith('.gz')
if (isExtensionPackage) {
try {
uploading.value = true
installing.value = true
showProgressModal.value = true
installProgress.value = 0
installMessage.value = t('Uploading file...')
installStatus.value = 'info'
const formData = new FormData()
formData.append('file', file)
installProgress.value = 20
installMessage.value = t('Saving package...')
const saveResponse = await axios.post('/jingrow/install-extension', formData, {
headers: {
...get_session_api_headers(),
'Content-Type': 'multipart/form-data'
},
timeout: 60000
})
if (!saveResponse.data.success) {
throw new Error(saveResponse.data.error || t('Failed to save package'))
}
const fileUrl = saveResponse.data.file_url
installProgress.value = 50
installMessage.value = t('Installing package...')
const installResponse = await axios.post('/api/action/jingrow.ai.utils.jlocal.install_package', {
package_file_url: fileUrl
}, {
headers: get_session_api_headers()
})
installProgress.value = 100
installMessage.value = t('Installation completed!')
installStatus.value = 'success'
if (installResponse.data.message && installResponse.data.message.success) {
clearFiles()
const result = installResponse.data.message
message.success(t('Package \'{0}\' installed successfully').replace('{0}', result.package_name))
} else {
throw new Error(installResponse.data.message?.error || t('Installation failed'))
}
} catch (error: any) {
installProgress.value = 100
installMessage.value = t('Installation failed!')
installStatus.value = 'error'
const errorMsg = error.response?.data?.detail ||
error.response?.data?.error ||
error.message ||
t('Upload failed')
message.error(errorMsg)
} finally {
uploading.value = false
installing.value = false
}
return
}
// 使
const apiEndpoint = '/jingrow/install/upload'
try {
@ -334,12 +258,12 @@ const startUpload = async () => {
const formData = new FormData()
formData.append('file', file)
if (appName.value.trim() && !isExtensionPackage) {
if (appName.value.trim()) {
formData.append('app_name', appName.value.trim())
}
installProgress.value = 30
installMessage.value = isExtensionPackage ? t('Installing extension package...') : t('Uploading file...')
installMessage.value = t('Uploading file...')
const response = await axios.post(apiEndpoint, formData, {
headers: {
...get_session_api_headers(),
@ -360,7 +284,7 @@ const startUpload = async () => {
if (response.data.success) {
clearFiles()
const resultAppName = isExtensionPackage ? response.data.package_name : response.data.app_name
const resultAppName = response.data.app_name
message.success(t('Package \'{0}\' installed successfully').replace('{0}', resultAppName))
} else {
throw new Error(response.data.error || t('Installation failed'))

View File

@ -8,6 +8,7 @@ from typing import Dict, Any, List, Optional
import logging
import tempfile
import os
import uuid
from pathlib import Path
import requests
import json
@ -82,34 +83,127 @@ async def install_app_from_upload(
):
try:
if not file.filename.lower().endswith('.zip'):
raise HTTPException(status_code=400, detail="只支持ZIP格式的安装包")
filename_lower = file.filename.lower()
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
# 支持zip、tar.gz、tgz、gz格式
if not (filename_lower.endswith('.zip') or
filename_lower.endswith('.tar.gz') or
filename_lower.endswith('.tgz') or
filename_lower.endswith('.gz')):
raise HTTPException(status_code=400, detail="只支持ZIP、TAR.GZ、TGZ、GZ格式的安装包")
# 获取项目根目录
current = Path(__file__).resolve()
root = current.parents[4] # apps/jingrow/jingrow/api/ -> apps/ -> root
tmp_dir = root / "tmp"
tmp_dir.mkdir(parents=True, exist_ok=True)
# 创建唯一的文件名
temp_filename = f"upload_{uuid.uuid4().hex[:8]}{Path(filename_lower).suffix}"
temp_file_path = tmp_dir / temp_filename
# 保存上传的文件到项目tmp目录
content = await file.read()
log_info(f"文件大小: {len(content)} 字节")
with open(temp_file_path, 'wb') as f:
f.write(content)
log_info(f"文件已保存到: {temp_file_path}, 存在: {temp_file_path.exists()}, 大小: {temp_file_path.stat().st_size if temp_file_path.exists() else 0}")
# 统一使用 install_app 函数处理所有格式
try:
content = await file.read()
temp_file.write(content)
temp_file.close()
result = install_app(temp_file.name, app_name)
result = install_app(str(temp_file_path), app_name)
if result.get('success'):
app_name_result = result.get('app_name')
backend_result = result.get('backend_result', {})
app_dir = backend_result.get('app_dir')
# 对齐扫描安装的执行链
try:
_import_app_package_and_pagetypes(app_name_result, result)
except Exception:
pass
# 1. 添加到 Local Installed Apps PageType
from jingrow.utils.jingrow_api import get_single_pagetype
pagetype_result = get_single_pagetype("Local Installed Apps")
if pagetype_result.get('success'):
config = pagetype_result.get('config', {})
apps_list = config.get('local_installed_apps', [])
else:
apps_list = []
# 检查是否已存在,如果存在则更新,否则添加
app_exists = False
for app in apps_list:
if app.get('app_name', '') == app_name_result:
app['app_version'] = '1.0.0'
app['git_branch'] = 'main'
app_exists = True
break
if not app_exists:
apps_list.append({
'app_name': app_name_result,
'app_version': '1.0.0',
'git_branch': 'main'
})
# 更新数据库
if await _update_local_installed_apps(apps_list):
log_info(f"已更新 Local Installed Apps: {app_name_result}")
# 2. 调用 sync_app_files 同步文件到数据库pagetype, package, module
log_info(f"准备同步应用文件: app_name={app_name_result}, app_dir={app_dir}")
if app_dir:
# 计算应用的后端目录
current = Path(__file__).resolve()
root = current.parents[4]
apps_dir = root / "apps"
backend_dir = apps_dir / app_name_result / app_name_result
if not backend_dir.exists():
backend_dir = apps_dir / app_name_result
log_info(f"后端目录: {backend_dir}, 存在: {backend_dir.exists()}")
if backend_dir.exists():
try:
api_url = f"{Config.jingrow_server_url}/api/action/jingrow.ai.utils.jlocal.sync_app_files"
log_info(f"调用 sync_app_files API: {api_url}, 参数: app_name={app_name_result}, app_path={str(backend_dir)}")
response = requests.post(
api_url,
json={'app_name': app_name_result, 'app_path': str(backend_dir), 'force': True},
headers=get_jingrow_api_headers(),
timeout=60
)
log_info(f"sync_app_files 响应: status={response.status_code}, body={response.text}")
if response.status_code == 200:
log_info(f"已同步应用文件到数据库: {app_name_result}, 响应: {response.json()}")
else:
log_error(f"同步应用文件失败: HTTP {response.status_code}, {response.text}")
except Exception as e:
import traceback
log_error(f"同步应用文件异常: {str(e)}, {traceback.format_exc()}")
else:
log_error(f"后端目录不存在: {backend_dir}")
else:
log_error(f"app_dir 为空,无法同步文件")
except Exception as e:
log_error(f"更新数据库失败: {str(e)}")
return result
else:
raise HTTPException(status_code=400, detail=result.get('error'))
error_msg = result.get('error', '未知错误')
log_error(f"安装失败: {error_msg}")
raise HTTPException(status_code=400, detail=error_msg)
finally:
# 清理上传的文件
try:
os.unlink(temp_file.name)
except:
pass
if temp_file_path.exists():
os.unlink(temp_file_path)
log_info(f"已删除临时文件: {temp_file_path}")
except Exception as e:
log_error(f"删除临时文件失败: {e}")
except HTTPException:
raise

View File

@ -8,6 +8,7 @@ import shutil
import tempfile
import zipfile
import tarfile
import uuid
from pathlib import Path
from typing import Dict, List, Any
from datetime import datetime
@ -36,7 +37,6 @@ def get_app_directories():
"""获取应用目录路径"""
project_root = Path(__file__).resolve().parents[4]
apps_dir = project_root / "apps"
apps_dir.mkdir(parents=True, exist_ok=True)
return apps_dir, apps_dir
@ -45,7 +45,15 @@ def get_app_directories():
@handle_errors
def extract_package(zip_path: str) -> Dict[str, Any]:
"""解压安装包 - 支持 ZIP 和 TAR.GZ"""
temp_dir = tempfile.mkdtemp(prefix="jingrow_app_install_")
# 获取项目根目录
project_root = Path(__file__).resolve().parents[4]
tmp_dir = project_root / "tmp"
tmp_dir.mkdir(parents=True, exist_ok=True)
# 创建唯一临时目录
temp_dir_name = f"app_install_{uuid.uuid4().hex[:8]}"
temp_dir = tmp_dir / temp_dir_name
temp_dir.mkdir(parents=True, exist_ok=True)
# 判断文件类型
if zip_path.endswith('.tar.gz') or zip_path.endswith('.tgz') or zip_path.endswith('.gz'):
@ -57,7 +65,7 @@ def extract_package(zip_path: str) -> Dict[str, Any]:
else:
return {'success': False, 'error': '不支持的文件格式'}
return {'success': True, 'temp_dir': temp_dir}
return {'success': True, 'temp_dir': str(temp_dir)}
@handle_errors
@ -74,7 +82,8 @@ def analyze_package(temp_dir: str) -> Dict[str, Any]:
'has_backend': False,
'has_frontend': False,
'backend_files': [],
'frontend_files': []
'frontend_files': [],
'root_dir': root_dir # 保存根目录路径
}
# 检查配置文件
@ -98,7 +107,8 @@ def analyze_package(temp_dir: str) -> Dict[str, Any]:
for root, dirs, files in os.walk(root_dir):
for file in files:
rel_path = os.path.relpath(os.path.join(root, file), root_dir)
if file.endswith(('.py', '.txt', '.cfg')):
if file.endswith(('.py', '.txt', '.cfg', '.json', '.md', '.LICENSE', '.yaml', '.yml')):
package_info['backend_files'].append(rel_path)
package_info['has_backend'] = True
elif file.endswith(('.vue', '.ts', '.js', '.css')):
@ -125,27 +135,49 @@ def install_files(temp_dir: str, app_name: str, package_info: Dict[str, Any], is
backend_dir.mkdir(parents=True, exist_ok=True)
frontend_dir.mkdir(parents=True, exist_ok=True)
# 获取根目录
root_dir = package_info.get('root_dir', temp_dir)
copied_files = []
# 复制后端文件
if package_info.get('has_backend', False):
for file_path in package_info.get('backend_files', []):
src_path = os.path.join(temp_dir, file_path)
dst_path = backend_dir / file_path
# file_path 是相对于 root_dir 的路径
# 例如jin/hooks.py
# root_dir 是 tmp/app_install_xxx/jin
# 所以 src_path = tmp/app_install_xxx/jin/jin/hooks.py
src_path = os.path.join(root_dir, file_path)
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dst_path)
copied_files.append(str(dst_path))
# dst_path 需要去掉 app_name 前缀
if file_path.startswith(f"{app_name}/"):
dst_path = backend_dir / file_path[len(app_name) + 1:]
else:
dst_path = backend_dir / file_path
if os.path.exists(src_path):
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dst_path)
copied_files.append(str(dst_path))
else:
log_error(f"源文件不存在: {src_path}, root_dir={root_dir}, file_path={file_path}")
# 复制前端文件
if package_info.get('has_frontend', False):
for file_path in package_info.get('frontend_files', []):
src_path = os.path.join(temp_dir, file_path)
dst_path = frontend_dir / file_path
src_path = os.path.join(root_dir, file_path)
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dst_path)
copied_files.append(str(dst_path))
# dst_path 需要去掉 app_name 前缀
if file_path.startswith(f"{app_name}/"):
dst_path = frontend_dir / file_path[len(app_name) + 1:]
else:
dst_path = frontend_dir / file_path
if os.path.exists(src_path):
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dst_path)
copied_files.append(str(dst_path))
else:
log_error(f"源文件不存在: {src_path}, root_dir={root_dir}, file_path={file_path}")
return {
'success': True,
@ -245,8 +277,15 @@ def install_app(uploaded_file_path: str, app_name: str = None) -> Dict[str, Any]
log_info(f"开始安装应用包: {uploaded_file_path}")
# 验证文件
if not os.path.exists(uploaded_file_path) or not uploaded_file_path.lower().endswith('.zip'):
return {'success': False, 'error': '无效的ZIP文件'}
if not os.path.exists(uploaded_file_path):
return {'success': False, 'error': '文件不存在'}
filename_lower = uploaded_file_path.lower()
if not (filename_lower.endswith('.zip') or
filename_lower.endswith('.tar.gz') or
filename_lower.endswith('.tgz') or
filename_lower.endswith('.gz')):
return {'success': False, 'error': '不支持的文件格式支持ZIP、TAR.GZ、TGZ、GZ格式'}
# 解压文件
extract_result = extract_package(uploaded_file_path)
@ -259,21 +298,35 @@ def install_app(uploaded_file_path: str, app_name: str = None) -> Dict[str, Any]
analyze_result = analyze_package(temp_dir)
if not analyze_result.get('success'):
cleanup_temp_dir(temp_dir)
log_error(f"分析包失败: {analyze_result.get('error')}")
return analyze_result
package_info = analyze_result['data']
log_info(f"包信息: {package_info}")
# 确定应用名称
if not app_name:
app_name = package_info.get('app_name')
if not app_name:
cleanup_temp_dir(temp_dir)
log_error(f"无法识别应用名称,包信息: {package_info}")
return {'success': False, 'error': '无法识别应用名称'}
# 检查是否已安装
log_info(f"应用名称: {app_name}")
# 如果应用已安装,先删除旧版本(允许覆盖安装)
if is_app_installed(app_name):
cleanup_temp_dir(temp_dir)
return {'success': False, 'error': f'应用 {app_name} 已安装'}
log_info(f"应用 {app_name} 已存在,将覆盖安装")
apps_dir, _ = get_app_directories()
app_dir = apps_dir / app_name
try:
if app_dir.exists():
shutil.rmtree(app_dir)
log_info(f"已删除旧版本应用: {app_dir}")
except Exception as e:
cleanup_temp_dir(temp_dir)
log_error(f"删除旧应用失败: {e}")
return {'success': False, 'error': f'删除旧应用失败: {str(e)}'}
# 安装后端文件
backend_result = None