From 2fc67a0082680552005e7878f581ce1569165288 Mon Sep 17 00:00:00 2001 From: jingrow Date: Mon, 27 Oct 2025 04:33:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BB=8Egit=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=AE=89=E8=A3=85=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + apps/jingrow/frontend/src/locales/zh-CN.json | 1 - .../frontend/src/views/dev/AppDetail.vue | 108 ++++++++---------- .../frontend/src/views/dev/AppMarketplace.vue | 82 +++++++++---- .../jingrow/api/local_app_installer.py | 107 +++++++++++++++++ apps/jingrow/jingrow/utils/app_installer.py | 25 ++++ 6 files changed, 244 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index 2bf207d..6451cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ test/ .env +# apps 目录:只跟踪 jingrow,忽略其他所有应用 +apps/* +!apps/jingrow/ +!apps/jingrow diff --git a/apps/jingrow/frontend/src/locales/zh-CN.json b/apps/jingrow/frontend/src/locales/zh-CN.json index ba839ec..6ef05c2 100644 --- a/apps/jingrow/frontend/src/locales/zh-CN.json +++ b/apps/jingrow/frontend/src/locales/zh-CN.json @@ -863,7 +863,6 @@ "应用创建成功": "应用创建成功", "应用创建失败": "应用创建失败", "Create a new application for the marketplace": "为应用市场创建新应用", - "Update": "更新", "App Title": "应用标题", "App Description": "应用描述", "Please enter application description (optional)": "请输入应用描述(可选)", diff --git a/apps/jingrow/frontend/src/views/dev/AppDetail.vue b/apps/jingrow/frontend/src/views/dev/AppDetail.vue index b1b1b4a..225b24f 100644 --- a/apps/jingrow/frontend/src/views/dev/AppDetail.vue +++ b/apps/jingrow/frontend/src/views/dev/AppDetail.vue @@ -12,17 +12,11 @@ {{ t('Back') }} - + - {{ isCurrentAppInstalled ? t('Update') : t('Install') }} + {{ t('Install') }} @@ -157,18 +151,8 @@ const installMessage = ref('') const installStatus = ref<'success' | 'error' | 'info'>('info') const showProgressModal = ref(false) -// 已安装应用集合 -const installedAppNames = ref>(new Set()) - const appName = computed(() => route.params.name as string) -// 检查当前应用是否已安装 -const isCurrentAppInstalled = computed(() => { - if (!app.value) return false - const appName = app.value.app_name || app.value.name || '' - return installedAppNames.value.has(appName.toLowerCase()) -}) - async function loadAppDetail() { loading.value = true error.value = '' @@ -209,8 +193,8 @@ function goBack() { } async function installApp() { - if (!app.value?.file_url) { - message.error(t('应用文件URL不存在')) + if (!app.value?.file_url && !app.value?.repository_url) { + message.error(t('应用文件URL或仓库地址不存在')) return } @@ -245,27 +229,55 @@ async function performInstall() { try { installing.value = true installProgress.value = 0 - installMessage.value = t('正在下载应用包...') + installMessage.value = t('正在准备安装...') installStatus.value = 'info' showProgressModal.value = true - // 下载阶段 - setTimeout(() => { - installProgress.value = 20 - }, 300) + let response - // 下载并安装 - installProgress.value = 30 - installMessage.value = t('正在安装应用...') + // 优先使用文件URL,否则使用git仓库 + if (app.value.file_url) { + installMessage.value = t('正在下载应用包...') + setTimeout(() => { + installProgress.value = 20 + }, 300) + + installProgress.value = 30 + installMessage.value = t('正在安装应用...') + + response = await axios.post('/jingrow/install-from-url', new URLSearchParams({ + url: app.value.file_url + }), { + headers: { + ...get_session_api_headers(), + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + } else if (app.value.repository_url) { + installMessage.value = t('正在克隆仓库...') + setTimeout(() => { + installProgress.value = 20 + }, 300) + + installProgress.value = 30 + installMessage.value = t('正在安装应用...') + + const params = new URLSearchParams({ + repo_url: app.value.repository_url, + branch: app.value.branch || 'main' + }) + + response = await axios.post('/jingrow/install-from-git', params, { + headers: { + ...get_session_api_headers(), + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + } - const response = await axios.post('/jingrow/install-from-url', new URLSearchParams({ - url: app.value.file_url - }), { - headers: { - ...get_session_api_headers(), - 'Content-Type': 'application/x-www-form-urlencoded' - } - }) + if (!response) { + throw new Error(t('无法确定安装方式')) + } // 更新进度到安装完成 installProgress.value = 100 @@ -277,9 +289,6 @@ async function performInstall() { installMessage.value = t('应用安装成功!') message.success(t('应用安装成功')) - // 刷新已安装应用列表 - loadInstalledApps() - setTimeout(() => { showProgressModal.value = false }, 2000) @@ -299,27 +308,8 @@ async function performInstall() { } } -// 加载已安装应用列表 -async function loadInstalledApps() { - try { - const response = await axios.get('/jingrow/installed-app-names') - if (response.data.success) { - const apps = response.data.apps || [] - installedAppNames.value = new Set(apps) - } - } catch (error) { - console.error('Load installed apps error:', error) - } -} - onMounted(() => { loadAppDetail() - loadInstalledApps() - - // 监听全局事件 - window.addEventListener('installedAppsUpdated', () => { - loadInstalledApps() - }) }) diff --git a/apps/jingrow/frontend/src/views/dev/AppMarketplace.vue b/apps/jingrow/frontend/src/views/dev/AppMarketplace.vue index d532492..a9bf574 100644 --- a/apps/jingrow/frontend/src/views/dev/AppMarketplace.vue +++ b/apps/jingrow/frontend/src/views/dev/AppMarketplace.vue @@ -246,8 +246,8 @@ function viewAppDetail(app: any) { } async function installApp(app: any) { - if (!app.file_url) { - message.error(t('应用文件URL不存在')) + if (!app.file_url && !app.repository_url) { + message.error(t('应用文件URL或仓库地址不存在')) return } @@ -282,27 +282,55 @@ async function performInstall(app: any) { try { installing.value = true installProgress.value = 0 - installMessage.value = t('正在下载应用包...') + installMessage.value = t('正在准备安装...') installStatus.value = 'info' showProgressModal.value = true - // 下载阶段 - setTimeout(() => { - installProgress.value = 20 - }, 300) + let response - // 下载并安装 - installProgress.value = 30 - installMessage.value = t('正在安装应用...') + // 优先使用文件URL,否则使用git仓库 + if (app.file_url) { + installMessage.value = t('正在下载应用包...') + setTimeout(() => { + installProgress.value = 20 + }, 300) + + installProgress.value = 30 + installMessage.value = t('正在安装应用...') + + response = await axios.post('/jingrow/install-from-url', new URLSearchParams({ + url: app.file_url + }), { + headers: { + ...get_session_api_headers(), + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + } else if (app.repository_url) { + installMessage.value = t('正在克隆仓库...') + setTimeout(() => { + installProgress.value = 20 + }, 300) + + installProgress.value = 30 + installMessage.value = t('正在安装应用...') + + const params = new URLSearchParams({ + repo_url: app.repository_url, + branch: app.branch || 'main' + }) + + response = await axios.post('/jingrow/install-from-git', params, { + headers: { + ...get_session_api_headers(), + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + } - const response = await axios.post('/jingrow/install-from-url', new URLSearchParams({ - url: app.file_url - }), { - headers: { - ...get_session_api_headers(), - 'Content-Type': 'application/x-www-form-urlencoded' - } - }) + if (!response) { + throw new Error(t('无法确定安装方式')) + } // 更新进度到安装完成 installProgress.value = 100 @@ -361,10 +389,23 @@ function truncateText(text: string, maxLength: number): string { // 加载已安装应用列表 async function loadInstalledApps() { try { + const cached = sessionStorage.getItem('installed_apps_names') + if (cached) { + const data = JSON.parse(cached) + if (Date.now() - data.timestamp < 30 * 60 * 1000) { + installedAppNames.value = new Set(data.apps || []) + return + } + } + const response = await axios.get('/jingrow/installed-app-names') if (response.data.success) { const apps = response.data.apps || [] installedAppNames.value = new Set(apps) + sessionStorage.setItem('installed_apps_names', JSON.stringify({ + apps, + timestamp: Date.now() + })) } } catch (error) { console.error('Load installed apps error:', error) @@ -380,11 +421,6 @@ function isAppInstalled(appName: string): boolean { onMounted(() => { loadApps() loadInstalledApps() - - // 监听全局事件 - window.addEventListener('installedAppsUpdated', () => { - loadInstalledApps() - }) }) // 监听搜索和排序变化 diff --git a/apps/jingrow/jingrow/api/local_app_installer.py b/apps/jingrow/jingrow/api/local_app_installer.py index 53dae00..b488c93 100644 --- a/apps/jingrow/jingrow/api/local_app_installer.py +++ b/apps/jingrow/jingrow/api/local_app_installer.py @@ -502,6 +502,113 @@ async def get_installed_apps(request: Request): raise HTTPException(status_code=500, detail=str(e)) +@router.post("/jingrow/install-from-git") +async def install_from_git(repo_url: str = Form(...), branch: str = Form('main')): + """从 git 仓库克隆并安装应用或扩展包""" + import subprocess + + current = Path(__file__).resolve() + root = current.parents[4] + tmp_dir = root / "tmp" + tmp_dir.mkdir(parents=True, exist_ok=True) + + # 创建临时目录用于克隆 + clone_dir = tmp_dir / f"git_clone_{uuid.uuid4().hex[:8]}" + + try: + # 使用 git clone 克隆仓库 + result = subprocess.run( + ['git', 'clone', '-b', branch, repo_url, str(clone_dir)], + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode != 0: + logger.error(f"Git clone failed: {result.stderr}") + raise HTTPException(status_code=400, detail=f"Git 克隆失败: {result.stderr}") + + # 直接使用克隆的目录进行分析和安装 + from jingrow.utils.app_installer import analyze_package, install_files, is_app_installed + + # 分析包结构 + package_info = analyze_package(str(clone_dir)) + if not package_info.get('success'): + raise HTTPException(status_code=400, detail=package_info.get('error', '无法识别应用')) + + app_name = package_info['data'].get('app_name') + if not app_name: + raise HTTPException(status_code=400, detail='无法识别应用名称') + + # 如果应用已安装,先删除旧版本 + if is_app_installed(app_name): + apps_dir, _ = get_app_directories() + app_dir = apps_dir / app_name + if app_dir.exists(): + shutil.rmtree(app_dir) + + # 安装后端文件 + backend_result = None + if package_info['data'].get('has_backend'): + backend_result = install_files(str(clone_dir), app_name, package_info['data'], is_backend=True) + if not backend_result.get('success'): + raise HTTPException(status_code=400, detail=backend_result.get('error')) + + # 安装前端文件 + frontend_result = None + if package_info['data'].get('has_frontend'): + frontend_result = install_files(str(clone_dir), app_name, package_info['data'], is_backend=False) + if not frontend_result.get('success'): + raise HTTPException(status_code=400, detail=frontend_result.get('error')) + + # 清理临时文件 + shutil.rmtree(clone_dir, ignore_errors=True) + + # 只有独立应用才需要注册到 Local Installed Apps + app_dir = backend_result.get('app_dir') if backend_result else None + + if app_dir: + try: + from jingrow.utils.jingrow_api import get_single_pagetype + pagetype_result = get_single_pagetype("Local Installed Apps") + apps_list = pagetype_result.get('config', {}).get('local_installed_apps', []) if pagetype_result.get('success') else [] + + app_exists = False + for app in apps_list: + if app.get('app_name', '') == app_name: + app.update({'app_version': '1.0.0', 'git_branch': branch, 'git_repo': repo_url}) + app_exists = True + break + + if not app_exists: + apps_list.append({'app_name': app_name, 'app_version': '1.0.0', 'git_branch': branch, 'git_repo': repo_url}) + + await _update_local_installed_apps(apps_list) + + # 同步文件到数据库 + _import_app_package_and_pagetypes(app_name, {'app_name': app_name, 'backend_result': backend_result, 'frontend_result': frontend_result}) + except Exception as e: + logger.error(f"Failed to register app: {e}") + + return { + 'success': True, + 'message': f'应用 {app_name} 安装成功', + 'app_name': app_name, + 'package_info': package_info['data'], + 'backend_result': backend_result, + 'frontend_result': frontend_result + } + + except subprocess.TimeoutExpired: + if clone_dir.exists(): + shutil.rmtree(clone_dir, ignore_errors=True) + raise HTTPException(status_code=400, detail="Git 克隆超时") + except Exception as e: + if clone_dir.exists(): + shutil.rmtree(clone_dir, ignore_errors=True) + raise HTTPException(status_code=500, detail=f"安装失败: {str(e)}") + + @router.post("/jingrow/install-from-url") async def install_from_url(url: str = Form(...)): """从URL安装应用或扩展包""" diff --git a/apps/jingrow/jingrow/utils/app_installer.py b/apps/jingrow/jingrow/utils/app_installer.py index 81e9483..88140b9 100644 --- a/apps/jingrow/jingrow/utils/app_installer.py +++ b/apps/jingrow/jingrow/utils/app_installer.py @@ -70,9 +70,14 @@ def extract_package(zip_path: str) -> Dict[str, Any]: @handle_errors def analyze_package(temp_dir: str) -> Dict[str, Any]: """分析安装包结构""" + print(f"[DEBUG] analyze_package called with temp_dir: {temp_dir}") + # 查找根目录 root_items = os.listdir(temp_dir) + print(f"[DEBUG] temp_dir contents: {root_items}") + 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 + print(f"[DEBUG] resolved root_dir: {root_dir}") package_info = { 'app_name': os.path.basename(root_dir), @@ -85,6 +90,8 @@ def analyze_package(temp_dir: str) -> Dict[str, Any]: 'root_dir': root_dir # 保存根目录路径 } + print(f"[DEBUG] initial app_name from basename: {package_info['app_name']}") + # 检查配置文件 if os.path.exists(os.path.join(root_dir, 'setup.py')): package_info['has_backend'] = True @@ -110,6 +117,24 @@ def analyze_package(temp_dir: str) -> Dict[str, Any]: if 'hooks.py' in files: hooks_path = os.path.join(root, 'hooks.py') break + + # 从 hooks.py 提取 app_name + print(f"[DEBUG] hooks_path: {hooks_path}, exists: {os.path.exists(hooks_path)}") + if os.path.exists(hooks_path): + try: + with open(hooks_path, 'r', encoding='utf-8') as f: + content = f.read() + import re + name_match = re.search(r'app_name\s*=\s*["\']([^"\']+)["\']', content) + if name_match: + package_info['app_name'] = name_match.group(1) + print(f"[DEBUG] extracted app_name from hooks.py: {package_info['app_name']}") + except Exception as e: + print(f"[DEBUG] failed to extract app_name from hooks.py: {e}") + pass + + print(f"[DEBUG] final app_name: {package_info['app_name']}") + package_info['has_hooks'] = os.path.exists(hooks_path) # 扫描文件