From 77cb098f80f17f74e7d4a7ba75df78bdbf70ab63 Mon Sep 17 00:00:00 2001 From: jingrow Date: Wed, 29 Oct 2025 03:23:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=B2=E5=AE=89=E8=A3=85=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E5=A2=9E=E5=8A=A0=E5=AF=BC=E5=87=BA=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=AE=89=E8=A3=85=E5=8C=85=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jingrow/frontend/src/locales/zh-CN.json | 2 +- .../frontend/src/views/dev/InstalledApps.vue | 43 +++++++++++- .../jingrow/api/local_app_installer.py | 37 +++++++++- .../jingrow/utils/export_app_package.py | 68 +++++++++++++++++++ 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 apps/jingrow/jingrow/utils/export_app_package.py diff --git a/apps/jingrow/frontend/src/locales/zh-CN.json b/apps/jingrow/frontend/src/locales/zh-CN.json index a79eeb3..e26d45f 100644 --- a/apps/jingrow/frontend/src/locales/zh-CN.json +++ b/apps/jingrow/frontend/src/locales/zh-CN.json @@ -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...": "上传中...", diff --git a/apps/jingrow/frontend/src/views/dev/InstalledApps.vue b/apps/jingrow/frontend/src/views/dev/InstalledApps.vue index 98992d9..7a23d9a 100644 --- a/apps/jingrow/frontend/src/views/dev/InstalledApps.vue +++ b/apps/jingrow/frontend/src/views/dev/InstalledApps.vue @@ -51,6 +51,7 @@ import { t } from '@/shared/i18n' const installedApps = ref([]) const loadingApps = ref(false) const uninstalling = ref(false) +const exporting = ref(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) { diff --git a/apps/jingrow/jingrow/api/local_app_installer.py b/apps/jingrow/jingrow/api/local_app_installer.py index bf80a67..d85f4a6 100644 --- a/apps/jingrow/jingrow/api/local_app_installer.py +++ b/apps/jingrow/jingrow/api/local_app_installer.py @@ -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)}") diff --git a/apps/jingrow/jingrow/utils/export_app_package.py b/apps/jingrow/jingrow/utils/export_app_package.py new file mode 100644 index 0000000..099b235 --- /dev/null +++ b/apps/jingrow/jingrow/utils/export_app_package.py @@ -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}" + }