已安装应用界面增加导出应用安装包的功能

This commit is contained in:
jingrow 2025-10-29 03:23:17 +08:00
parent c8bf0ea7dd
commit 77cb098f80
4 changed files with 145 additions and 5 deletions

View File

@ -787,7 +787,7 @@
"Package '{0}' uninstalled successfully": "扩展包 '{0}' 卸载成功",
"Package '{0}' installed successfully": "扩展包 '{0}' 安装成功",
"App Installer": "应用扩展安装",
"App Installer": "应用安装",
"Upload and install applications to your local Jingrow environment": "上传并安装应用或扩展到您的本地 Jingrow 环境",
"Upload App Package": "上传应用或扩展包",
"Uploading...": "上传中...",

View File

@ -51,6 +51,7 @@ import { t } from '@/shared/i18n'
const installedApps = ref<any[]>([])
const loadingApps = ref(false)
const uninstalling = ref(false)
const exporting = ref<string | null>(null)
const message = useMessage()
const dialog = useDialog()
@ -77,12 +78,20 @@ const columns: DataTableColumns = [
{
title: t('Actions'),
key: 'actions',
width: 100,
width: 280,
render: (row: any) => {
return h('div', { class: 'action-buttons' }, [
h(NButton, {
size: 'small',
type: 'default',
loading: exporting.value === row.name,
disabled: row.name === 'jingrow',
onClick: () => exportApp(row)
}, { default: () => t('Export Package') }),
h(NButton, {
size: 'small',
type: 'error',
style: 'margin-left: 8px;',
disabled: !row.uninstallable,
onClick: () => uninstallApp(row)
}, { default: () => row.uninstallable ? t('Uninstall') : t('System App') })
@ -118,6 +127,38 @@ const refreshApps = () => {
}
const exportApp = async (app: any) => {
//
if (app.name === 'jingrow') {
message.warning(t('System App') + ' ' + t('cannot be exported'))
return
}
try {
exporting.value = app.name
const response = await axios.post(`/jingrow/export-app-package/${app.name}`, {}, {
headers: get_session_api_headers()
})
if (response.data.success) {
message.success(t('App package exported successfully: {0}').replace('{0}', response.data.filename || app.name))
//
if (response.data.filename) {
const fileUrl = `/files/${response.data.filename}`
window.open(fileUrl, '_blank')
}
} else {
message.error(response.data.error || t('Failed to export app package'))
}
} catch (error: any) {
console.error('Export app error:', error)
message.error(error.response?.data?.error || t('Failed to export app package'))
} finally {
exporting.value = null
}
}
const uninstallApp = async (app: any) => {
//
if (!app.uninstallable) {

View File

@ -9,15 +9,20 @@ import logging
import tempfile
import os
import uuid
from pathlib import Path
import requests
import re
import json
import shutil
import re
import subprocess
import unicodedata
import traceback
from pathlib import Path
from datetime import datetime
import requests
from jingrow.utils.app_installer import install_app, get_installed_apps as get_apps, get_app_directories, ensure_package_and_module
from jingrow.utils.jingrow_api import 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.utils.export_app_package import export_app_package_from_local
from jingrow.config import Config
logger = logging.getLogger(__name__)
@ -1133,3 +1138,29 @@ async def create_app(
except Exception as e:
raise HTTPException(status_code=500, detail=f"发布失败: {str(e)}")
@router.post("/jingrow/export-app-package/{app_name}")
async def export_app_package(app_name: str):
"""导出应用安装包 - 直接打包本地应用源码"""
try:
logger.info(f"开始导出应用安装包: {app_name}")
# 获取应用目录
current = Path(__file__).resolve()
root = current.parents[4]
apps_dir = root / "apps"
# 调用辅助函数
result = export_app_package_from_local(app_name, apps_dir)
if not result.get("success"):
raise HTTPException(status_code=400, detail=result.get("error", "导出失败"))
return result
except HTTPException:
raise
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)}")

View File

@ -0,0 +1,68 @@
"""
导出应用安装包功能
"""
from pathlib import Path
import subprocess
import shutil
from datetime import datetime
def export_app_package_from_local(app_name: str, apps_dir: Path):
"""从本地应用目录打包应用安装包"""
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
root = Path(__file__).resolve().parents[4]
# 使用 tmp 作为临时打包目录
tmp_dir = root / "tmp"
tmp_dir.mkdir(parents=True, exist_ok=True)
# files 目录用于存放最终导出文件
files_dir = root / "apps" / "jingrow" / "frontend" / "public" / "files"
files_dir.mkdir(parents=True, exist_ok=True)
app_source_dir = apps_dir / app_name
if not app_source_dir.exists():
return {
"success": False,
"error": f"本地应用目录不存在: {app_source_dir}"
}
# 创建临时打包目录
final_dir = tmp_dir / app_name
if final_dir.exists():
shutil.rmtree(final_dir)
final_dir.mkdir(parents=True, exist_ok=True)
# 复制整个应用(包含后端和前端)
source = app_source_dir
for item in source.iterdir():
if item.name in ['__pycache__', '.git', '.DS_Store']:
continue
dst = final_dir / item.name
if item.is_dir():
shutil.copytree(item, dst, dirs_exist_ok=True)
else:
shutil.copy2(item, dst)
# 打包
final_filename = f"{app_name}-{timestamp}.tar.gz"
tmp_tar = tmp_dir / final_filename
subprocess.check_output(
["tar", "czf", str(tmp_tar), app_name],
cwd=str(tmp_dir)
)
# 移动到 files 目录
final_tar = files_dir / final_filename
shutil.move(str(tmp_tar), str(final_tar))
# 清理临时目录
shutil.rmtree(final_dir, ignore_errors=True)
return {
"success": True,
"filename": final_filename,
"file_path": str(final_tar),
"message": f"应用安装包已导出到: {final_tar}"
}