实现一键上传安装扩展包到jingrow数据库的功能

This commit is contained in:
jingrow 2025-10-26 14:59:20 +08:00
parent d3465d8210
commit d26c797624
3 changed files with 448 additions and 16 deletions

View File

@ -19,7 +19,7 @@
ref="uploadRef" ref="uploadRef"
:file-list="fileList" :file-list="fileList"
:max="1" :max="1"
accept=".zip" accept=".zip,.tar.gz,.tgz,.gz"
:show-file-list="false" :show-file-list="false"
:on-before-upload="beforeUpload" :on-before-upload="beforeUpload"
:on-change="handleFileChange" :on-change="handleFileChange"
@ -32,17 +32,17 @@
<Icon icon="tabler:cloud-upload" /> <Icon icon="tabler:cloud-upload" />
</n-icon> </n-icon>
<n-text style="font-size: 16px"> <n-text style="font-size: 16px">
{{ uploading ? t('Uploading...') : t('Click or drag ZIP file to this area to upload') }} {{ uploading ? t('Uploading...') : t('Click or drag package file to this area to upload') }}
</n-text> </n-text>
<n-p depth="3" style="margin: 8px 0 0 0"> <n-p depth="3" style="margin: 8px 0 0 0">
{{ t('Support for ZIP format only') }} {{ t('Support for ZIP, TAR.GZ, and GZ format') }}
</n-p> </n-p>
</div> </div>
</n-upload-dragger> </n-upload-dragger>
</n-upload> </n-upload>
<!-- 应用名称输入 --> <!-- 应用名称输入 - 仅普通应用包需要 -->
<div class="app-name-input" v-if="fileList.length > 0"> <div class="app-name-input" v-if="fileList.length > 0 && !isExtensionFile">
<n-form-item :label="t('App Name (Optional)')"> <n-form-item :label="t('App Name (Optional)')">
<n-input <n-input
v-model:value="appName" v-model:value="appName"
@ -128,6 +128,7 @@ const installProgress = ref(0)
const installMessage = ref('') const installMessage = ref('')
const installStatus = ref<'success' | 'error' | 'info'>('info') const installStatus = ref<'success' | 'error' | 'info'>('info')
const showProgressModal = ref(false) const showProgressModal = ref(false)
const isExtensionFile = ref(false) //
// App // App
const localApps = ref<any[]>([]) const localApps = ref<any[]>([])
@ -180,8 +181,12 @@ const beforeUpload = (data: { file: UploadFileInfo }) => {
const file = data.file.file const file = data.file.file
if (!file) return false if (!file) return false
if (!file.name.toLowerCase().endsWith('.zip')) { const fileName = file.name.toLowerCase()
message.error(t('Only ZIP files are supported')) const allowedExtensions = ['.zip', '.tar.gz', '.tgz', '.gz']
const isValidFile = allowedExtensions.some(ext => fileName.endsWith(ext))
if (!isValidFile) {
message.error(t('Only ZIP and TAR.GZ files are supported'))
return false return false
} }
@ -190,6 +195,19 @@ 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) {
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 = ''
}
}
} }
const customUpload = async (_options: any) => { const customUpload = async (_options: any) => {
@ -209,6 +227,92 @@ const startUpload = async () => {
return return
} }
// API endpoint
const fileName = file.name.toLowerCase()
const isExtensionPackage = fileName.endsWith('.tar.gz') || fileName.endsWith('.tgz') || fileName.endsWith('.gz')
// install_package
if (isExtensionPackage) {
try {
uploading.value = true
installing.value = true
showProgressModal.value = true
installProgress.value = 0
installMessage.value = t('Uploading file...')
installStatus.value = 'info'
// public/files
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 // 60
})
console.log('Save response:', saveResponse.data)
if (!saveResponse.data.success) {
throw new Error(saveResponse.data.error || t('Failed to save package'))
}
const fileUrl = saveResponse.data.file_url
console.log('File URL:', fileUrl)
installProgress.value = 50
installMessage.value = t('Installing package...')
// install_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'
console.error('Upload error:', error)
console.error('Error response data:', error.response?.data)
const errorMsg = error.response?.data?.detail ||
error.response?.data?.error ||
error.message ||
t('Upload failed')
console.error('Final error message:', errorMsg)
message.error(errorMsg)
} finally {
uploading.value = false
installing.value = false
}
return // 使
}
const apiEndpoint = '/jingrow/install/upload'
try { try {
uploading.value = true uploading.value = true
installing.value = true installing.value = true
@ -220,15 +324,16 @@ const startUpload = async () => {
// FormData // FormData
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
if (appName.value.trim()) { if (appName.value.trim() && !isExtensionPackage) {
// app_name
formData.append('app_name', appName.value.trim()) formData.append('app_name', appName.value.trim())
} }
installProgress.value = 30 installProgress.value = 30
installMessage.value = t('Uploading file...') installMessage.value = isExtensionPackage ? t('Installing extension package...') : t('Uploading file...')
// //
const response = await axios.post('/jingrow/install/upload', formData, { const response = await axios.post(apiEndpoint, formData, {
headers: { headers: {
...get_session_api_headers(), ...get_session_api_headers(),
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
@ -249,7 +354,8 @@ const startUpload = async () => {
// //
clearFiles() clearFiles()
message.success(t('App \'{0}\' installed successfully').replace('{0}', response.data.app_name)) const resultAppName = isExtensionPackage ? response.data.package_name : response.data.app_name
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'))
} }
@ -260,7 +366,17 @@ const startUpload = async () => {
installStatus.value = 'error' installStatus.value = 'error'
console.error('Upload error:', error) console.error('Upload error:', error)
message.error(error.response?.data?.detail || error.message || t('Upload failed')) console.error('Error response:', error.response)
console.error('Error details:', error.response?.data)
//
const errorDetail = error.response?.data?.detail ||
error.response?.data?.message ||
error.message ||
t('Upload failed')
console.error('Final error detail:', errorDetail)
message.error(errorDetail)
} finally { } finally {
uploading.value = false uploading.value = false
installing.value = false installing.value = false
@ -270,6 +386,7 @@ const startUpload = async () => {
const clearFiles = () => { const clearFiles = () => {
fileList.value = [] fileList.value = []
appName.value = '' appName.value = ''
isExtensionFile.value = false
uploadRef.value?.clear() uploadRef.value?.clear()
} }

View File

@ -14,7 +14,7 @@ import json
import shutil import shutil
import re import re
from jingrow.utils.app_installer import install_app, get_installed_apps as get_apps, get_app_directories from jingrow.utils.app_installer import install_app, get_installed_apps as get_apps, get_app_directories, install_extension_package
from jingrow.utils.jingrow_api import log_info, log_error from jingrow.utils.jingrow_api import log_info, log_error
from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers, get_jingrow_cloud_api_url, get_jingrow_api_headers from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers, get_jingrow_cloud_api_url, get_jingrow_api_headers
from jingrow.config import Config from jingrow.config import Config
@ -419,6 +419,92 @@ async def get_app_info(request: Request, app_name: str):
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
async def _install_extension_package_in_api(package_path: str, original_filename: str) -> Dict[str, Any]:
"""保存扩展包并立即安装"""
import shutil
import requests
try:
log_info(f"保存扩展包文件: {original_filename}")
# 复制文件到 public/files 目录
# 从 jingrow-framework/apps/jingrow/jingrow/api/local_app_installer.py
# 到 /home/jingrow/jingrow-bench/sites/test001/public/files/
current = Path(__file__).resolve()
# current: /home/dev/jingrow-framework/apps/jingrow/jingrow/api/local_app_installer.py
# 需要回到 /home/jingrow/jingrow-bench
# 如果当前在 framework 目录,则使用相对路径找到 jingrow-bench
if 'jingrow-framework' in str(current):
# 从 framework 目录回到 jingrow-bench
jingrow_bench_path = Path('/home/jingrow/jingrow-bench')
else:
# 从 apps/jingrow/... 到 jingrow-bench
project_root = current.parents[6] if current.parts.count('apps') > 1 else current.parents[5]
jingrow_bench_path = project_root
target_dir = jingrow_bench_path / "sites" / "test001" / "public" / "files"
target_dir.mkdir(parents=True, exist_ok=True)
target_path = target_dir / original_filename
# 复制文件
shutil.copy2(package_path, target_path)
log_info(f"文件已保存到: {target_path}")
# 立即调用 jlocal API 安装扩展包
try:
from jingrow.utils.jingrow_api import get_jingrow_api_headers
headers = get_jingrow_api_headers()
# 调用 jlocal.install_package
api_url = f"{Config.jingrow_server_url}/api/action/jingrow.ai.utils.jlocal.install_package"
file_url = f"/files/{original_filename}"
response = requests.post(
api_url,
json={'package_file_url': file_url},
headers=headers,
timeout=60
)
if response.status_code == 200:
result = response.json()
if result.get('message', {}).get('success'):
log_info(f"扩展包安装成功: {result.get('message', {}).get('package_name')}")
return {
'success': True,
'message': f'扩展包安装成功',
'package_name': result.get('message', {}).get('package_name'),
'file_url': file_url,
'imported_files': result.get('message', {}).get('imported_files', []),
'file_count': result.get('message', {}).get('file_count', 0)
}
else:
error_msg = result.get('message', {}).get('error', '未知错误')
log_error(f"安装失败: {error_msg}")
return {'success': False, 'error': error_msg}
else:
log_error(f"API调用失败: HTTP {response.status_code}")
return {'success': False, 'error': f'API调用失败: HTTP {response.status_code}'}
except Exception as api_error:
log_error(f"调用安装API失败: {str(api_error)}")
return {
'success': True,
'message': f'扩展包已保存到 public/files 目录',
'file_url': f'/files/{original_filename}',
'file_path': str(target_path),
'note': '文件已上传,请手动在 jingrow 应用中使用 Package Import 功能导入'
}
except Exception as e:
log_error(f"保存扩展包失败: {str(e)}")
return {'success': False, 'error': f'保存文件失败: {str(e)}'}
async def _remove_from_database(app_name: str) -> Dict[str, Any]: async def _remove_from_database(app_name: str) -> Dict[str, Any]:
"""从数据库中删除应用记录""" """从数据库中删除应用记录"""
try: try:
@ -555,6 +641,105 @@ async def get_app_meta():
raise HTTPException(status_code=500, detail=f"获取元数据失败: {str(e)}") raise HTTPException(status_code=500, detail=f"获取元数据失败: {str(e)}")
@router.post("/jingrow/install-extension")
async def install_extension_package_api(request: Request, file: UploadFile = File(...)):
"""安装扩展包到数据库"""
temp_file_path = None
try:
log_info(f"开始处理上传的扩展包: {file.filename}")
# 验证文件类型
if not file.filename:
raise HTTPException(status_code=400, detail="文件名不能为空")
filename_lower = file.filename.lower()
if not filename_lower.endswith(('.tar.gz', '.tgz', '.gz')):
raise HTTPException(status_code=400, detail=f"只支持TAR.GZ格式的扩展包当前文件: {filename_lower}")
# 保存上传文件到临时目录
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.tar.gz')
temp_file_path = temp_file.name
try:
content = await file.read()
if not content:
raise HTTPException(status_code=400, detail="文件内容为空")
temp_file.write(content)
temp_file.close()
log_info(f"文件已保存到临时位置: {temp_file_path}, 大小: {len(content)} 字节")
# 保存文件到 jingrow-bench/sites/test001/public/files/
target_dir = Path('/home/jingrow/jingrow-bench/sites/test001/public/files')
target_dir.mkdir(parents=True, exist_ok=True)
target_path = target_dir / file.filename
# 复制文件
shutil.copy2(temp_file_path, target_path)
# 修复文件权限和所有者
import pwd
try:
jingrow_user = pwd.getpwnam('jingrow')
os.chown(target_path, jingrow_user.pw_uid, jingrow_user.pw_gid)
os.chmod(target_path, 0o644)
log_info(f"文件权限已修复: owner=jingrow, permissions=644")
except Exception as perm_error:
log_error(f"修复文件权限失败: {perm_error}")
log_info(f"文件已保存到: {target_path}")
# 调用 install_package API 安装
from jingrow.utils.jingrow_api import get_jingrow_api_headers
import requests
api_url = f"{Config.jingrow_server_url}/api/action/jingrow.ai.utils.jlocal.install_package"
headers = get_jingrow_api_headers()
file_url = f"/files/{file.filename}"
response = requests.post(
api_url,
json={'package_file_url': file_url},
headers=headers,
timeout=60
)
if response.status_code == 200:
result_data = response.json()
if result_data.get('message', {}).get('success'):
log_info(f"扩展包安装成功: {result_data['message'].get('package_name')}")
return {
'success': True,
'package_name': result_data['message']['package_name'],
'file_url': file_url,
'file_count': result_data['message'].get('file_count', 0)
}
else:
error_msg = result_data.get('message', {}).get('error', '未知错误')
log_error(f"安装失败: {error_msg}")
raise HTTPException(status_code=400, detail=error_msg)
else:
log_error(f"API调用失败: HTTP {response.status_code}")
raise HTTPException(status_code=500, detail=f'安装API调用失败: HTTP {response.status_code}')
finally:
# 清理临时文件
if temp_file_path:
try:
os.unlink(temp_file_path)
log_info(f"临时文件已删除: {temp_file_path}")
except Exception as cleanup_error:
log_error(f"清理临时文件失败: {cleanup_error}")
except HTTPException:
raise
except Exception as e:
import traceback
error_detail = traceback.format_exc()
log_error(f"安装扩展包失败: {str(e)}\n{error_detail}")
raise HTTPException(status_code=500, detail=f"安装扩展包失败: {str(e)}")
@router.post("/jingrow/upload-image") @router.post("/jingrow/upload-image")
async def upload_image(file: UploadFile = File(...)): async def upload_image(file: UploadFile = File(...)):
"""上传应用图片""" """上传应用图片"""

View File

@ -7,6 +7,7 @@ import json
import shutil import shutil
import tempfile import tempfile
import zipfile import zipfile
import tarfile
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
@ -43,11 +44,18 @@ 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"""
temp_dir = tempfile.mkdtemp(prefix="jingrow_app_install_") temp_dir = tempfile.mkdtemp(prefix="jingrow_app_install_")
# 判断文件类型
if zip_path.endswith('.tar.gz') or zip_path.endswith('.tgz') or zip_path.endswith('.gz'):
with tarfile.open(zip_path, 'r:gz') as tar_ref:
tar_ref.extractall(temp_dir)
elif zip_path.endswith('.zip'):
with zipfile.ZipFile(zip_path, 'r') as zip_ref: with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir) zip_ref.extractall(temp_dir)
else:
return {'success': False, 'error': '不支持的文件格式'}
return {'success': True, 'temp_dir': temp_dir} return {'success': True, 'temp_dir': temp_dir}
@ -303,3 +311,125 @@ def install_app(uploaded_file_path: str, app_name: str = None) -> Dict[str, Any]
except Exception as e: except Exception as e:
log_error(f"安装应用失败: {str(e)}") log_error(f"安装应用失败: {str(e)}")
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
def install_extension_package(package_path: str) -> Dict[str, Any]:
"""
直接安装扩展包到数据库不走文件系统复制
Args:
package_path: 扩展包文件路径tar.gz
Returns:
Dict: 安装结果
"""
try:
log_info(f"开始安装扩展包: {package_path}")
# 验证文件
if not os.path.exists(package_path):
return {'success': False, 'error': '文件不存在'}
# 解压文件
extract_result = extract_package(package_path)
if not extract_result.get('success'):
return extract_result
temp_dir = extract_result['temp_dir']
# 查找根目录(通常是包名)
root_items = os.listdir(temp_dir)
if not root_items:
cleanup_temp_dir(temp_dir)
return {'success': False, 'error': '压缩包为空'}
# 找到顶层目录
root_dir = os.path.join(temp_dir, root_items[0]) if len(root_items) == 1 and os.path.isdir(os.path.join(temp_dir, root_items[0])) else temp_dir
package_name = os.path.basename(root_dir)
# 查找 package.json 文件
package_json_path = os.path.join(root_dir, f"{package_name}.json")
if not os.path.exists(package_json_path):
# 尝试查找所有json文件找到pagetype为Package的
for root, dirs, files in os.walk(root_dir):
for file in files:
if file.endswith('.json'):
try:
with open(os.path.join(root, file), 'r') as f:
data = json.load(f)
if data.get('pagetype') == 'Package':
package_json_path = os.path.join(root, file)
break
except:
pass
if os.path.exists(package_json_path):
break
if not os.path.exists(package_json_path):
cleanup_temp_dir(temp_dir)
return {'success': False, 'error': '找不到 Package.json 文件'}
# 导入 Package
try:
import jingrow
from jingrow.modules.import_file import import_pg, import_file_by_path
from jingrow.model.sync import get_pg_files
# 确保 jingrow 环境已初始化
if not hasattr(jingrow, 'db') or jingrow.db is None:
cleanup_temp_dir(temp_dir)
return {'success': False, 'error': 'jingrow 环境未初始化,请在 API 层调用'}
with open(package_json_path, 'r', encoding='utf-8') as f:
pg_dict = json.load(f)
# 验证 Package 数据
if not pg_dict.get('pagetype') == 'Package':
cleanup_temp_dir(temp_dir)
return {'success': False, 'error': f'无效的 Package 文件pagetype 为: {pg_dict.get("pagetype")}'}
# 导入 Package 到数据库
package_doc = import_pg(pg_dict, ignore_version=True)
jingrow.flags.package = package_doc
# 收集所有 pagetype 文件
files = []
for module in os.listdir(root_dir):
module_path = os.path.join(root_dir, module)
if os.path.isdir(module_path):
files = get_pg_files(files, module_path)
log_info(f"找到 {len(files)} 个 pagetype 文件待导入")
# 导入所有文件
imported_files = []
for file in files:
try:
import_file_by_path(file, force=True, ignore_version=True)
imported_files.append(file)
except Exception as e:
log_error(f"导入文件失败 {file}: {str(e)}")
# 继续导入其他文件,不中断
# 清理临时文件
cleanup_temp_dir(temp_dir)
log_info(f"扩展包 {package_name} 安装成功,导入了 {len(imported_files)} 个文件")
return {
'success': True,
'message': f'扩展包 {package_name} 安装成功',
'package_name': package_name,
'imported_files': imported_files,
'file_count': len(imported_files)
}
except Exception as e:
cleanup_temp_dir(temp_dir)
import traceback
error_detail = traceback.format_exc()
log_error(f"导入失败: {str(e)}")
return {'success': False, 'error': f'导入失败: {str(e)}', 'detail': error_detail}
except Exception as e:
log_error(f"安装扩展包失败: {str(e)}")
return {'success': False, 'error': str(e)}