实现应用的安装,卸载
This commit is contained in:
parent
3abe83fa11
commit
96731ac0ac
@ -818,6 +818,10 @@
|
||||
"Are you sure you want to uninstall": "您确定要卸载",
|
||||
"Are you sure you want to uninstall '{0}'? This action cannot be undone.": "您确定要卸载 '{0}' 吗?此操作无法撤销。",
|
||||
"App '{0}' uninstalled successfully": "应用 '{0}' 卸载成功",
|
||||
"System App": "系统应用",
|
||||
"cannot be uninstalled": "不允许卸载",
|
||||
"Version": "版本",
|
||||
"Git Branch": "Git分支",
|
||||
"This action cannot be undone.": "此操作无法撤销。",
|
||||
"Not installed": "未安装",
|
||||
|
||||
|
||||
@ -33,52 +33,13 @@
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 应用详情模态框 -->
|
||||
<n-modal v-model:show="showDetailModal" preset="card" style="width: 600px">
|
||||
<template #header>
|
||||
<h3>{{ selectedApp?.name || t('App Details') }}</h3>
|
||||
</template>
|
||||
|
||||
<div v-if="selectedApp" class="app-detail">
|
||||
<n-descriptions :column="1" bordered>
|
||||
<n-descriptions-item :label="t('App Name')">
|
||||
{{ selectedApp.name }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Type')">
|
||||
{{ getAppTypeText(selectedApp.type) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Backend Path')">
|
||||
{{ selectedApp.path || t('Not installed') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Frontend Path')">
|
||||
{{ selectedApp.frontend_path || t('Not installed') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Version')">
|
||||
{{ selectedApp.version || t('Unknown') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Description')">
|
||||
{{ selectedApp.description || t('No description') }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</div>
|
||||
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="showDetailModal = false">{{ t('Close') }}</n-button>
|
||||
<n-button type="error" @click="uninstallApp(selectedApp)" :loading="uninstalling">
|
||||
{{ t('Uninstall') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, h } from 'vue'
|
||||
import {
|
||||
NIcon, NButton, NSpace, NCard, NDataTable, NModal,
|
||||
NDescriptions, NDescriptionsItem, useMessage, useDialog,
|
||||
NIcon, NButton, NCard, NDataTable, useMessage, useDialog,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
@ -89,8 +50,6 @@ import { t } from '@/shared/i18n'
|
||||
// 响应式数据
|
||||
const installedApps = ref<any[]>([])
|
||||
const loadingApps = ref(false)
|
||||
const showDetailModal = ref(false)
|
||||
const selectedApp = ref<any>(null)
|
||||
const uninstalling = ref(false)
|
||||
|
||||
const message = useMessage()
|
||||
@ -104,38 +63,29 @@ const columns: DataTableColumns = [
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: t('Type'),
|
||||
key: 'type',
|
||||
title: t('Version'),
|
||||
key: 'version',
|
||||
width: 100,
|
||||
render: (row: any) => row.version || '1.0.0'
|
||||
},
|
||||
{
|
||||
title: t('Git Branch'),
|
||||
key: 'git_branch',
|
||||
width: 120,
|
||||
render: (row: any) => getAppTypeText(row.type)
|
||||
},
|
||||
{
|
||||
title: t('Backend Path'),
|
||||
key: 'path',
|
||||
ellipsis: true,
|
||||
render: (row: any) => row.path || t('Not installed')
|
||||
},
|
||||
{
|
||||
title: t('Frontend Path'),
|
||||
key: 'frontend_path',
|
||||
ellipsis: true,
|
||||
render: (row: any) => row.frontend_path || t('Not installed')
|
||||
render: (row: any) => row.git_branch || 'main'
|
||||
},
|
||||
{
|
||||
title: t('Actions'),
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
width: 100,
|
||||
render: (row: any) => {
|
||||
return h('div', { class: 'action-buttons' }, [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
onClick: () => showAppDetail(row)
|
||||
}, { default: () => t('Details') }),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
disabled: !row.uninstallable,
|
||||
onClick: () => uninstallApp(row)
|
||||
}, { default: () => t('Uninstall') })
|
||||
}, { default: () => row.uninstallable ? t('Uninstall') : t('System App') })
|
||||
])
|
||||
}
|
||||
}
|
||||
@ -186,6 +136,12 @@ const showAppDetail = async (app: any) => {
|
||||
}
|
||||
|
||||
const uninstallApp = async (app: any) => {
|
||||
// 检查是否为系统应用
|
||||
if (!app.uninstallable) {
|
||||
message.warning(t('System App') + ' ' + t('cannot be uninstalled'))
|
||||
return
|
||||
}
|
||||
|
||||
dialog.warning({
|
||||
title: t('Uninstall App'),
|
||||
content: t('Are you sure you want to uninstall \'{0}\'? This action cannot be undone.').replace('{0}', app.name),
|
||||
|
||||
@ -22,6 +22,16 @@ from jingrow.config import Config
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# 常量定义
|
||||
JINGROW_APP_NAME = 'jingrow'
|
||||
DEFAULT_VERSION = '1.0.0'
|
||||
DEFAULT_BRANCH = 'main'
|
||||
SYSTEM_APP_TYPE = 'system'
|
||||
INSTALLED_APP_TYPE = 'installed'
|
||||
|
||||
# 全局缓存,避免重复检查
|
||||
_jingrow_registered_cache = None
|
||||
|
||||
|
||||
@router.post("/jingrow/install/upload")
|
||||
async def install_app_from_upload(
|
||||
@ -182,26 +192,37 @@ async def install_local_app(request: Request, app_name: str):
|
||||
|
||||
# 将App信息添加到Local Installed Apps PageType
|
||||
try:
|
||||
if not local_installed_apps:
|
||||
# 创建Local Installed Apps实例
|
||||
from jingrow import new_pg
|
||||
local_installed_apps = new_pg("Local Installed Apps")
|
||||
from jingrow.utils.jingrow_api import get_single_pagetype
|
||||
|
||||
# 添加新的已安装App记录
|
||||
from jingrow import new_pg
|
||||
installed_app = new_pg("Installed Application")
|
||||
installed_app.app_name = app_name
|
||||
installed_app.app_version = "1.0.0" # 可以从hooks.py读取版本
|
||||
installed_app.git_branch = "main" # 默认分支
|
||||
# 获取当前应用列表
|
||||
result = get_single_pagetype("Local Installed Apps")
|
||||
if result.get('success'):
|
||||
config = result.get('config', {})
|
||||
apps_list = config.get('local_installed_apps', [])
|
||||
else:
|
||||
apps_list = []
|
||||
|
||||
# 添加到Local Installed Apps的表格字段
|
||||
if not hasattr(local_installed_apps, 'local_installed_apps') or not local_installed_apps.local_installed_apps:
|
||||
local_installed_apps.local_installed_apps = []
|
||||
local_installed_apps.local_installed_apps.append(installed_app)
|
||||
local_installed_apps.save()
|
||||
# 检查是否已存在
|
||||
for app in apps_list:
|
||||
if app.get('app_name', '') == app_name:
|
||||
log_info(f"应用 {app_name} 已存在于数据库中")
|
||||
break
|
||||
else:
|
||||
# 添加新应用
|
||||
new_app = {
|
||||
'app_name': app_name,
|
||||
'app_version': "1.0.0",
|
||||
'git_branch': "main"
|
||||
}
|
||||
apps_list.append(new_app)
|
||||
|
||||
if await _update_local_installed_apps(apps_list):
|
||||
log_info(f"应用 {app_name} 已注册到数据库")
|
||||
else:
|
||||
log_error("更新数据库失败")
|
||||
except Exception as e:
|
||||
# 如果PageType操作失败,记录日志但不阻止安装
|
||||
log_error(f"Failed to update Local Installed Apps PageType: {str(e)}")
|
||||
log_error(f"注册应用到数据库失败: {str(e)}")
|
||||
# 数据库操作失败不应该阻止安装,但需要记录错误
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@ -216,10 +237,59 @@ async def install_local_app(request: Request, app_name: str):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
async def _update_local_installed_apps(apps_list):
|
||||
"""更新Local Installed Apps数据库记录"""
|
||||
import requests
|
||||
from jingrow.utils.auth import get_jingrow_api_headers
|
||||
from jingrow.config import Config
|
||||
|
||||
api_url = f"{Config.jingrow_server_url}/api/data/Local Installed Apps/Local Installed Apps"
|
||||
payload = {"local_installed_apps": apps_list}
|
||||
response = requests.put(api_url, json=payload, headers=get_jingrow_api_headers(), timeout=10)
|
||||
return response.status_code == 200
|
||||
|
||||
async def ensure_jingrow_registered():
|
||||
"""确保jingrow应用已注册到数据库中(带缓存)"""
|
||||
global _jingrow_registered_cache
|
||||
|
||||
# 如果已经检查过且成功,直接返回
|
||||
if _jingrow_registered_cache is True:
|
||||
return
|
||||
|
||||
try:
|
||||
from jingrow.utils.jingrow_api import get_single_pagetype
|
||||
|
||||
result = get_single_pagetype("Local Installed Apps")
|
||||
if not result.get('success'):
|
||||
return
|
||||
|
||||
apps = result.get('config', {}).get('local_installed_apps', [])
|
||||
|
||||
# 检查jingrow是否已存在
|
||||
if not any(app.get('app_name') == JINGROW_APP_NAME for app in apps):
|
||||
apps.append({'app_name': JINGROW_APP_NAME, 'app_version': DEFAULT_VERSION, 'git_branch': DEFAULT_BRANCH})
|
||||
if await _update_local_installed_apps(apps):
|
||||
log_info("jingrow应用已自动注册到数据库")
|
||||
_jingrow_registered_cache = True
|
||||
else:
|
||||
log_error("注册jingrow应用失败")
|
||||
_jingrow_registered_cache = False
|
||||
else:
|
||||
log_info("jingrow应用已在数据库中")
|
||||
_jingrow_registered_cache = True
|
||||
|
||||
except Exception as e:
|
||||
log_error(f"检查jingrow应用注册状态失败: {str(e)}")
|
||||
_jingrow_registered_cache = False
|
||||
|
||||
|
||||
@router.get("/jingrow/installed-apps")
|
||||
async def get_installed_apps(request: Request):
|
||||
"""获取已安装的应用列表 - 通过get_single API获取"""
|
||||
try:
|
||||
# 确保jingrow应用已注册
|
||||
await ensure_jingrow_registered()
|
||||
|
||||
# 通过get_single API获取Local Installed Apps数据
|
||||
from jingrow.utils.jingrow_api import get_single_pagetype
|
||||
|
||||
@ -237,14 +307,16 @@ async def get_installed_apps(request: Request):
|
||||
config = result.get('config', {})
|
||||
local_installed_apps = config.get('local_installed_apps', [])
|
||||
|
||||
apps = []
|
||||
for app in local_installed_apps:
|
||||
apps.append({
|
||||
apps = [
|
||||
{
|
||||
'name': app.get('app_name', ''),
|
||||
'version': app.get('app_version', '1.0.0'),
|
||||
'git_branch': app.get('git_branch', 'main'),
|
||||
'type': 'installed'
|
||||
})
|
||||
'type': SYSTEM_APP_TYPE if app.get('app_name') == JINGROW_APP_NAME else INSTALLED_APP_TYPE,
|
||||
'uninstallable': app.get('app_name') != JINGROW_APP_NAME
|
||||
}
|
||||
for app in local_installed_apps
|
||||
]
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@ -265,17 +337,42 @@ async def uninstall_app(request: Request, app_name: str):
|
||||
try:
|
||||
log_info(f"开始卸载应用: {app_name}")
|
||||
|
||||
# 检查应用是否存在
|
||||
# 禁止卸载系统应用
|
||||
if app_name == JINGROW_APP_NAME:
|
||||
raise HTTPException(status_code=403, detail=f"系统应用 '{JINGROW_APP_NAME}' 不允许卸载")
|
||||
|
||||
# 检查应用是否存在(文件系统或数据库)
|
||||
backend_dir, frontend_dir = get_app_directories()
|
||||
backend_app_dir = backend_dir / app_name / app_name
|
||||
# 检查多种可能的目录结构
|
||||
possible_backend_dirs = [
|
||||
backend_dir / app_name / app_name, # apps/myapp/myapp/
|
||||
backend_dir / app_name / app_name / app_name, # apps/myapp/myapp/myapp/
|
||||
]
|
||||
frontend_app_dir = frontend_dir / app_name / "frontend"
|
||||
|
||||
if not backend_app_dir.exists() and not frontend_app_dir.exists():
|
||||
# 找到实际存在的后端目录
|
||||
backend_app_dir = None
|
||||
for possible_dir in possible_backend_dirs:
|
||||
if possible_dir.exists():
|
||||
backend_app_dir = possible_dir
|
||||
break
|
||||
|
||||
# 检查数据库中是否有记录
|
||||
from jingrow.utils.jingrow_api import get_single_pagetype
|
||||
db_result = get_single_pagetype("Local Installed Apps")
|
||||
app_exists_in_db = False
|
||||
if db_result.get('success'):
|
||||
config = db_result.get('config', {})
|
||||
local_installed_apps = config.get('local_installed_apps', [])
|
||||
app_exists_in_db = any(app.get('app_name', '') == app_name for app in local_installed_apps)
|
||||
|
||||
# 如果文件不存在且数据库也没有记录,则报错
|
||||
if not backend_app_dir and not frontend_app_dir.exists() and not app_exists_in_db:
|
||||
raise HTTPException(status_code=404, detail=f"应用 {app_name} 不存在")
|
||||
|
||||
# 删除后端文件
|
||||
backend_result = {'success': True, 'message': '无后端文件'}
|
||||
if backend_app_dir.exists():
|
||||
if backend_app_dir:
|
||||
try:
|
||||
shutil.rmtree(backend_app_dir)
|
||||
backend_result = {'success': True, 'message': '后端文件删除成功'}
|
||||
@ -363,44 +460,36 @@ async def get_app_info(request: Request, app_name: str):
|
||||
|
||||
|
||||
async def _remove_from_database(app_name: str) -> Dict[str, Any]:
|
||||
|
||||
"""从数据库中删除应用记录"""
|
||||
try:
|
||||
api_url = f"{Config.jingrow_server_url}/api/action/jingrow.core.pagetype.local_installed_apps.local_installed_apps.delete"
|
||||
# 使用API方式操作数据库
|
||||
from jingrow.utils.jingrow_api import get_single_pagetype
|
||||
|
||||
headers = get_jingrow_api_headers()
|
||||
# 获取当前数据
|
||||
result = get_single_pagetype("Local Installed Apps")
|
||||
if not result.get('success'):
|
||||
return {'success': True, 'message': '未找到Local Installed Apps记录'}
|
||||
|
||||
# 先查询记录
|
||||
query_url = f"{Config.jingrow_server_url}/api/action/jingrow.core.pagetype.local_installed_apps.local_installed_apps.get_list"
|
||||
query_payload = {
|
||||
'filters': {
|
||||
'app_name': app_name,
|
||||
'user_site': Config.jingrow_site or 'local'
|
||||
}
|
||||
}
|
||||
config = result.get('config', {})
|
||||
local_installed_apps = config.get('local_installed_apps', [])
|
||||
|
||||
query_response = requests.post(query_url, json=query_payload, headers=headers, timeout=30)
|
||||
log_info(f"当前应用列表: {local_installed_apps}")
|
||||
|
||||
if query_response.status_code == 200:
|
||||
query_result = query_response.json()
|
||||
if isinstance(query_result, dict) and 'message' in query_result:
|
||||
query_result = query_result['message']
|
||||
|
||||
if query_result.get('success') and query_result.get('data'):
|
||||
# 删除找到的记录
|
||||
for record in query_result['data']:
|
||||
delete_payload = {'name': record['name']}
|
||||
delete_response = requests.post(api_url, json=delete_payload, headers=headers, timeout=30)
|
||||
|
||||
if delete_response.status_code == 200:
|
||||
return {'success': True, 'message': '数据库记录删除成功'}
|
||||
else:
|
||||
return {'success': False, 'error': f'删除记录失败: HTTP {delete_response.status_code}'}
|
||||
# 查找要删除的应用
|
||||
original_count = len(local_installed_apps)
|
||||
local_installed_apps = [app for app in local_installed_apps if app.get('app_name', '') != app_name]
|
||||
|
||||
if len(local_installed_apps) < original_count:
|
||||
if await _update_local_installed_apps(local_installed_apps):
|
||||
log_info(f"应用 {app_name} 已从Local Installed Apps删除")
|
||||
return {'success': True, 'message': '数据库记录删除成功'}
|
||||
else:
|
||||
return {'success': True, 'message': '未找到数据库记录'}
|
||||
return {'success': False, 'error': '更新数据库失败'}
|
||||
else:
|
||||
return {'success': False, 'error': f'查询记录失败: HTTP {query_response.status_code}'}
|
||||
return {'success': True, 'message': '未找到要删除的应用记录'}
|
||||
|
||||
except Exception as e:
|
||||
log_error(f"从数据库删除应用记录失败: {str(e)}")
|
||||
return {'success': False, 'error': f'数据库操作异常: {str(e)}'}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user