实现从git仓库安装应用

This commit is contained in:
jingrow 2025-10-27 04:33:12 +08:00
parent bbb037e708
commit 2fc67a0082
6 changed files with 244 additions and 83 deletions

4
.gitignore vendored
View File

@ -36,3 +36,7 @@ test/
.env
# apps 目录:只跟踪 jingrow忽略其他所有应用
apps/*
!apps/jingrow/
!apps/jingrow

View File

@ -863,7 +863,6 @@
"应用创建成功": "应用创建成功",
"应用创建失败": "应用创建失败",
"Create a new application for the marketplace": "为应用市场创建新应用",
"Update": "更新",
"App Title": "应用标题",
"App Description": "应用描述",
"Please enter application description (optional)": "请输入应用描述(可选)",

View File

@ -12,17 +12,11 @@
</template>
{{ t('Back') }}
</n-button>
<n-button
:type="isCurrentAppInstalled ? 'warning' : 'primary'"
@click="installApp"
size="medium"
>
<n-button type="primary" @click="installApp" size="medium">
<template #icon>
<n-icon>
<Icon :icon="isCurrentAppInstalled ? 'tabler:refresh' : 'tabler:download'" />
</n-icon>
<n-icon><Icon icon="tabler:download" /></n-icon>
</template>
{{ isCurrentAppInstalled ? t('Update') : t('Install') }}
{{ t('Install') }}
</n-button>
</div>
</div>
@ -157,18 +151,8 @@ const installMessage = ref('')
const installStatus = ref<'success' | 'error' | 'info'>('info')
const showProgressModal = ref(false)
//
const installedAppNames = ref<Set<string>>(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()
})
})
</script>

View File

@ -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()
})
})
//

View File

@ -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安装应用或扩展包"""

View File

@ -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)
# 扫描文件