实现应用的安装,卸载

This commit is contained in:
jingrow 2025-10-26 01:22:01 +08:00
parent 3abe83fa11
commit 96731ac0ac
3 changed files with 167 additions and 118 deletions

View File

@ -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": "未安装",

View File

@ -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),

View File

@ -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)}'}