修复应用安装界面无法通过上传安装包安装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[] }) => { const handleFileChange = (options: { fileList: UploadFileInfo[] }) => {
fileList.value = options.fileList fileList.value = options.fileList
// 使
if (fileList.value.length > 0) { if (fileList.value.length > 0) {
const fileName = fileList.value[0].file?.name || '' isExtensionFile.value = false
isExtensionFile.value = fileName.toLowerCase().endsWith('.tar.gz') ||
fileName.toLowerCase().endsWith('.tgz') ||
fileName.toLowerCase().endsWith('.gz')
if (isExtensionFile.value) {
appName.value = ''
}
} }
} }
@ -251,77 +245,7 @@ const startUpload = async () => {
return 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' const apiEndpoint = '/jingrow/install/upload'
try { try {
@ -334,12 +258,12 @@ const startUpload = async () => {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
if (appName.value.trim() && !isExtensionPackage) { if (appName.value.trim()) {
formData.append('app_name', appName.value.trim()) formData.append('app_name', appName.value.trim())
} }
installProgress.value = 30 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, { const response = await axios.post(apiEndpoint, formData, {
headers: { headers: {
...get_session_api_headers(), ...get_session_api_headers(),
@ -360,7 +284,7 @@ const startUpload = async () => {
if (response.data.success) { if (response.data.success) {
clearFiles() 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)) message.success(t('Package \'{0}\' installed successfully').replace('{0}', resultAppName))
} else { } else {
throw new Error(response.data.error || t('Installation failed')) 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 logging
import tempfile import tempfile
import os import os
import uuid
from pathlib import Path from pathlib import Path
import requests import requests
import json import json
@ -82,34 +83,127 @@ async def install_app_from_upload(
): ):
try: try:
if not file.filename.lower().endswith('.zip'): filename_lower = file.filename.lower()
raise HTTPException(status_code=400, detail="只支持ZIP格式的安装包")
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: try:
content = await file.read() result = install_app(str(temp_file_path), app_name)
temp_file.write(content)
temp_file.close()
result = install_app(temp_file.name, app_name)
if result.get('success'): if result.get('success'):
app_name_result = result.get('app_name') app_name_result = result.get('app_name')
backend_result = result.get('backend_result', {})
app_dir = backend_result.get('app_dir')
# 对齐扫描安装的执行链
try: try:
_import_app_package_and_pagetypes(app_name_result, result) # 1. 添加到 Local Installed Apps PageType
except Exception: from jingrow.utils.jingrow_api import get_single_pagetype
pass 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 return result
else: 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: finally:
# 清理上传的文件
try: try:
os.unlink(temp_file.name) if temp_file_path.exists():
except: os.unlink(temp_file_path)
pass log_info(f"已删除临时文件: {temp_file_path}")
except Exception as e:
log_error(f"删除临时文件失败: {e}")
except HTTPException: except HTTPException:
raise raise

View File

@ -8,6 +8,7 @@ import shutil
import tempfile import tempfile
import zipfile import zipfile
import tarfile import tarfile
import uuid
from pathlib import Path from pathlib import Path
from typing import Dict, List, Any from typing import Dict, List, Any
from datetime import datetime from datetime import datetime
@ -36,7 +37,6 @@ def get_app_directories():
"""获取应用目录路径""" """获取应用目录路径"""
project_root = Path(__file__).resolve().parents[4] project_root = Path(__file__).resolve().parents[4]
apps_dir = project_root / "apps" apps_dir = project_root / "apps"
apps_dir.mkdir(parents=True, exist_ok=True) apps_dir.mkdir(parents=True, exist_ok=True)
return apps_dir, apps_dir return apps_dir, apps_dir
@ -45,7 +45,15 @@ def get_app_directories():
@handle_errors @handle_errors
def extract_package(zip_path: str) -> Dict[str, Any]: def extract_package(zip_path: str) -> Dict[str, Any]:
"""解压安装包 - 支持 ZIP 和 TAR.GZ""" """解压安装包 - 支持 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'): 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: else:
return {'success': False, 'error': '不支持的文件格式'} return {'success': False, 'error': '不支持的文件格式'}
return {'success': True, 'temp_dir': temp_dir} return {'success': True, 'temp_dir': str(temp_dir)}
@handle_errors @handle_errors
@ -74,7 +82,8 @@ def analyze_package(temp_dir: str) -> Dict[str, Any]:
'has_backend': False, 'has_backend': False,
'has_frontend': False, 'has_frontend': False,
'backend_files': [], '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 root, dirs, files in os.walk(root_dir):
for file in files: for file in files:
rel_path = os.path.relpath(os.path.join(root, file), root_dir) 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['backend_files'].append(rel_path)
package_info['has_backend'] = True package_info['has_backend'] = True
elif file.endswith(('.vue', '.ts', '.js', '.css')): 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) backend_dir.mkdir(parents=True, exist_ok=True)
frontend_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 = [] copied_files = []
# 复制后端文件 # 复制后端文件
if package_info.get('has_backend', False): if package_info.get('has_backend', False):
for file_path in package_info.get('backend_files', []): for file_path in package_info.get('backend_files', []):
src_path = os.path.join(temp_dir, file_path) # file_path 是相对于 root_dir 的路径
dst_path = backend_dir / file_path # 例如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) # dst_path 需要去掉 app_name 前缀
shutil.copy2(src_path, dst_path) if file_path.startswith(f"{app_name}/"):
copied_files.append(str(dst_path)) 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): if package_info.get('has_frontend', False):
for file_path in package_info.get('frontend_files', []): for file_path in package_info.get('frontend_files', []):
src_path = os.path.join(temp_dir, file_path) src_path = os.path.join(root_dir, file_path)
dst_path = frontend_dir / file_path
dst_path.parent.mkdir(parents=True, exist_ok=True) # dst_path 需要去掉 app_name 前缀
shutil.copy2(src_path, dst_path) if file_path.startswith(f"{app_name}/"):
copied_files.append(str(dst_path)) 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 { return {
'success': True, '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}") log_info(f"开始安装应用包: {uploaded_file_path}")
# 验证文件 # 验证文件
if not os.path.exists(uploaded_file_path) or not uploaded_file_path.lower().endswith('.zip'): if not os.path.exists(uploaded_file_path):
return {'success': False, 'error': '无效的ZIP文件'} 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) 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) analyze_result = analyze_package(temp_dir)
if not analyze_result.get('success'): if not analyze_result.get('success'):
cleanup_temp_dir(temp_dir) cleanup_temp_dir(temp_dir)
log_error(f"分析包失败: {analyze_result.get('error')}")
return analyze_result return analyze_result
package_info = analyze_result['data'] package_info = analyze_result['data']
log_info(f"包信息: {package_info}")
# 确定应用名称 # 确定应用名称
if not app_name: if not app_name:
app_name = package_info.get('app_name') app_name = package_info.get('app_name')
if not app_name: if not app_name:
cleanup_temp_dir(temp_dir) cleanup_temp_dir(temp_dir)
log_error(f"无法识别应用名称,包信息: {package_info}")
return {'success': False, 'error': '无法识别应用名称'} return {'success': False, 'error': '无法识别应用名称'}
# 检查是否已安装 log_info(f"应用名称: {app_name}")
# 如果应用已安装,先删除旧版本(允许覆盖安装)
if is_app_installed(app_name): if is_app_installed(app_name):
cleanup_temp_dir(temp_dir) log_info(f"应用 {app_name} 已存在,将覆盖安装")
return {'success': False, 'error': 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 backend_result = None