实现从git仓库安装应用
This commit is contained in:
parent
bbb037e708
commit
2fc67a0082
4
.gitignore
vendored
4
.gitignore
vendored
@ -36,3 +36,7 @@ test/
|
||||
.env
|
||||
|
||||
|
||||
# apps 目录:只跟踪 jingrow,忽略其他所有应用
|
||||
apps/*
|
||||
!apps/jingrow/
|
||||
!apps/jingrow
|
||||
|
||||
@ -863,7 +863,6 @@
|
||||
"应用创建成功": "应用创建成功",
|
||||
"应用创建失败": "应用创建失败",
|
||||
"Create a new application for the marketplace": "为应用市场创建新应用",
|
||||
"Update": "更新",
|
||||
"App Title": "应用标题",
|
||||
"App Description": "应用描述",
|
||||
"Please enter application description (optional)": "请输入应用描述(可选)",
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
// 监听搜索和排序变化
|
||||
|
||||
@ -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安装应用或扩展包"""
|
||||
|
||||
@ -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)
|
||||
|
||||
# 扫描文件
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user