Fix tool installation and deletion issues

This commit is contained in:
jingrow 2025-11-21 16:43:20 +08:00
parent 58d44c5291
commit f1c8054754
5 changed files with 430 additions and 52 deletions

View File

@ -1,8 +1,10 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { Router } from 'vue-router'
import axios from 'axios'
import { t } from '../i18n'
import { registerToolRoute, unregisterToolRoute, registerAllToolRoutes, ensureToolRoutes } from '../utils/dynamicRoutes'
import { get_session_api_headers } from '../api/auth'
export interface Tool {
id: string
@ -19,7 +21,8 @@ export interface Tool {
hidden?: boolean
// 市场工具相关
fromMarketplace?: boolean
marketplaceId?: string
marketplaceId?: string // 市场的唯一ID用于识别
toolName?: string // 实际的 tool_name用于删除文件系统
version?: string
author?: string
rating?: number
@ -155,9 +158,47 @@ export const useToolsStore = defineStore('tools', () => {
}
// 删除用户工具
function deleteUserTool(toolId: string, router?: Router) {
async function deleteUserTool(toolId: string, router?: Router) {
const tool = userTools.value.find(t => t.id === toolId)
console.log('删除工具:', { toolId, tool, fromMarketplace: tool?.fromMarketplace, marketplaceId: tool?.marketplaceId, toolName: tool?.toolName, componentPath: tool?.componentPath })
// 确定工具名称tool_name优先使用 toolName否则从 componentPath 提取
let toolName: string | null = null
if (tool) {
// 优先使用 toolName实际的 tool_name用于文件系统
if (tool.toolName) {
toolName = tool.toolName
}
// 否则从 componentPath 提取工具名称格式tools/{tool_name}/{tool_name}.vue
else if (tool.componentPath) {
const match = tool.componentPath.match(/tools\/([^\/]+)\//)
if (match && match[1]) {
toolName = match[1]
}
}
}
// 如果找到了工具名称,尝试删除文件系统
if (toolName) {
try {
console.log('调用后端API删除工具文件工具名称:', toolName)
// 调用后端API删除工具文件
const response = await axios.post(`/jingrow/uninstall-tool/${toolName}`, {}, {
headers: {
...get_session_api_headers()
}
})
console.log('删除工具文件成功:', response.data)
} catch (error: any) {
console.error('删除工具文件失败:', error.response?.data || error.message || '未知错误')
// 即使删除文件失败也继续删除store中的工具
}
} else {
console.log('无法确定工具名称,跳过文件删除')
}
userTools.value = userTools.value.filter(t => t.id !== toolId)
// 重新分配 order
const defaultToolsCount = getDefaultTools().filter(

View File

@ -232,6 +232,11 @@ const hiddenTools = computed(() => toolsStore.hiddenTools)
onMounted(() => {
loading.value = false
//
window.addEventListener('installedToolsUpdated', () => {
// store computed
})
})
//
@ -403,10 +408,15 @@ function handleDeleteTool(tool: Tool) {
content: `${t('Are you sure you want to delete tool')} "${tool.name}"?`,
positiveText: t('Delete'),
negativeText: t('Cancel'),
onPositiveClick: () => {
//
toolsStore.deleteUserTool(tool.id, router)
message.success(t('Tool deleted successfully'))
onPositiveClick: async () => {
try {
//
await toolsStore.deleteUserTool(tool.id, router)
message.success(t('Tool deleted successfully'))
} catch (error) {
console.error('Delete tool error:', error)
message.error(t('Failed to delete tool'))
}
}
})
}

View File

@ -134,6 +134,15 @@
</div>
</div>
<!-- 安装进度弹窗 -->
<InstallProgressModal
v-model="showProgressModal"
:progress="installProgress"
:message="installMessage"
:status="installStatus"
:installing="installing"
/>
</div>
</template>
@ -146,6 +155,7 @@ import { Icon } from '@iconify/vue'
import axios from 'axios'
import { t } from '@/shared/i18n'
import { useToolsStore } from '@/shared/stores/tools'
import InstallProgressModal from './InstallProgressModal.vue'
const route = useRoute()
const router = useRouter()
@ -156,6 +166,13 @@ const loading = ref(true)
const error = ref('')
const tool = ref<any>(null)
//
const installing = ref(false)
const installProgress = ref(0)
const installMessage = ref('')
const installStatus = ref<'success' | 'error' | 'info'>('info')
const showProgressModal = ref(false)
//
const installedToolNames = ref<Set<string>>(new Set())
@ -209,31 +226,138 @@ function goBack() {
}
async function installTool() {
if (!tool.value.file_url && !tool.value.repository_url) {
message.error(t('工具文件URL或仓库地址不存在'))
return
}
performInstall()
}
async function performInstall() {
try {
//
const toolData = {
id: tool.value.name || `tool-${Date.now()}`,
name: tool.value.title || tool.value.name,
description: tool.value.description,
category: tool.value.category,
icon: tool.value.icon,
color: tool.value.color || '#e5e7eb',
type: 'route', // route
routeName: tool.value.route_name, // addUserTool
isDefault: false,
fromMarketplace: true,
marketplaceId: tool.value.name
installing.value = true
installProgress.value = 0
installMessage.value = t('正在准备安装...')
installStatus.value = 'info'
showProgressModal.value = true
let response
// 使URL使git
if (tool.value.file_url) {
installMessage.value = t('正在下载工具包...')
setTimeout(() => {
installProgress.value = 20
}, 300)
installProgress.value = 30
installMessage.value = t('正在安装工具...')
response = await axios.post('/jingrow/install-tool-from-url', new URLSearchParams({
url: tool.value.file_url
}), {
headers: {
...get_session_api_headers(),
'Content-Type': 'application/x-www-form-urlencoded'
}
})
} else if (tool.value.repository_url) {
installMessage.value = t('正在克隆仓库...')
setTimeout(() => {
installProgress.value = 20
}, 300)
installProgress.value = 30
installMessage.value = t('正在安装工具...')
// install-tool-from-git API
message.warning(t('从Git仓库安装工具功能暂未实现'))
installing.value = false
installStatus.value = 'error'
installMessage.value = t('从Git仓库安装工具功能暂未实现')
setTimeout(() => {
showProgressModal.value = false
}, 3000)
return
}
// router 便
toolsStore.addUserTool(toolData, router)
message.success(t('Tool installed successfully'))
if (!response) {
throw new Error(t('无法确定安装方式'))
}
//
loadInstalledTools()
//
installProgress.value = 100
if (response.data.success) {
//
installing.value = false
installStatus.value = 'success'
installMessage.value = t('工具安装成功!')
message.success(t('工具安装成功'))
// toolsStore
// tool_name tool.value.name ID
const toolName = response.data.tool_name || tool.value.tool_name // tool_name
const toolTitle = response.data.tool_title || tool.value.title || tool.value.name
const marketplaceId = tool.value.name // ID
if (!toolName) {
console.error('无法获取工具名称 (tool_name)')
throw new Error('无法获取工具名称')
}
// store 使ID
const existingTool = toolsStore.userTools.find(
t => t.marketplaceId === marketplaceId && t.fromMarketplace
)
if (!existingTool) {
//
const toolData = {
id: marketplaceId || `tool-${Date.now()}`, // 使IDid
name: toolTitle,
description: tool.value.description,
category: tool.value.category,
icon: tool.value.icon,
color: tool.value.color || '#e5e7eb',
type: 'route' as const,
routeName: tool.value.route_name, // addUserTool
isDefault: false,
fromMarketplace: true,
marketplaceId: marketplaceId, // ID
toolName: toolName, // tool_name
componentPath: `tools/${toolName}/${toolName}.vue` // 使 tool_name
}
// store
toolsStore.addUserTool(toolData, router, toolData.componentPath)
}
//
loadInstalledTools()
//
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('installedToolsUpdated'))
}
setTimeout(() => {
showProgressModal.value = false
}, 2000)
} else {
throw new Error(response.data.error || t('安装失败'))
}
} catch (error: any) {
console.error('Failed to install tool:', error)
message.error(error.response?.data?.detail || t('Failed to install tool'))
console.error('Install tool error:', error)
installing.value = false
installStatus.value = 'error'
installMessage.value = error.response?.data?.detail || error.message || t('安装失败')
message.error(error.response?.data?.detail || t('安装失败'))
setTimeout(() => {
showProgressModal.value = false
}, 3000)
}
}

View File

@ -137,20 +137,31 @@
</n-empty>
</div>
</div>
<!-- 安装进度弹窗 -->
<InstallProgressModal
v-model="showProgressModal"
:progress="installProgress"
:message="installMessage"
:status="installStatus"
:installing="installing"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage } from 'naive-ui'
import { NInput, NButton, NIcon, NSpin, NEmpty, NSelect, NPagination, useMessage, useDialog } from 'naive-ui'
import { Icon } from '@iconify/vue'
import axios from 'axios'
import { t } from '@/shared/i18n'
import { get_session_api_headers } from '@/shared/api/auth'
import { useToolsStore } from '@/shared/stores/tools'
import InstallProgressModal from './InstallProgressModal.vue'
const message = useMessage()
const dialog = useDialog()
const router = useRouter()
const toolsStore = useToolsStore()
@ -162,6 +173,13 @@ const page = ref(1)
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '20'))
const sortBy = ref('creation desc')
//
const installing = ref(false)
const installProgress = ref(0)
const installMessage = ref('')
const installStatus = ref<'success' | 'error' | 'info'>('info')
const showProgressModal = ref(false)
//
const installedToolNames = ref<Set<string>>(new Set())
@ -225,31 +243,138 @@ function viewToolDetail(tool: any) {
}
async function installTool(tool: any) {
if (!tool.file_url && !tool.repository_url) {
message.error(t('工具文件URL或仓库地址不存在'))
return
}
performInstall(tool)
}
async function performInstall(tool: any) {
try {
//
const toolData = {
id: tool.name || `tool-${Date.now()}`,
name: tool.title || tool.name,
description: tool.description,
category: tool.category,
icon: tool.icon,
color: tool.color || '#e5e7eb',
type: 'route', // route
routeName: tool.route_name, // addUserTool
isDefault: false,
fromMarketplace: true,
marketplaceId: tool.name
installing.value = true
installProgress.value = 0
installMessage.value = t('正在准备安装...')
installStatus.value = 'info'
showProgressModal.value = true
let response
// 使URL使git
if (tool.file_url) {
installMessage.value = t('正在下载工具包...')
setTimeout(() => {
installProgress.value = 20
}, 300)
installProgress.value = 30
installMessage.value = t('正在安装工具...')
response = await axios.post('/jingrow/install-tool-from-url', new URLSearchParams({
url: tool.file_url
}), {
headers: {
...get_session_api_headers(),
'Content-Type': 'application/x-www-form-urlencoded'
}
})
} else if (tool.repository_url) {
installMessage.value = t('正在克隆仓库...')
setTimeout(() => {
installProgress.value = 20
}, 300)
installProgress.value = 30
installMessage.value = t('正在安装工具...')
// install-tool-from-git API
message.warning(t('从Git仓库安装工具功能暂未实现'))
installing.value = false
installStatus.value = 'error'
installMessage.value = t('从Git仓库安装工具功能暂未实现')
setTimeout(() => {
showProgressModal.value = false
}, 3000)
return
}
// router 便
toolsStore.addUserTool(toolData, router)
message.success(t('Tool installed successfully'))
if (!response) {
throw new Error(t('无法确定安装方式'))
}
//
loadInstalledTools()
//
installProgress.value = 100
if (response.data.success) {
//
installing.value = false
installStatus.value = 'success'
installMessage.value = t('工具安装成功!')
message.success(t('工具安装成功'))
// toolsStore
// tool_name tool.name ID
const toolName = response.data.tool_name || tool.tool_name // tool_name
const toolTitle = response.data.tool_title || tool.title || tool.name
const marketplaceId = tool.name // ID
if (!toolName) {
console.error('无法获取工具名称 (tool_name)')
throw new Error('无法获取工具名称')
}
// store 使 tool_name
const existingTool = toolsStore.userTools.find(
t => t.marketplaceId === marketplaceId && t.fromMarketplace
)
if (!existingTool) {
//
const toolData = {
id: marketplaceId || `tool-${Date.now()}`, // 使IDid
name: toolTitle,
description: tool.description,
category: tool.category,
icon: tool.icon,
color: tool.color || '#e5e7eb',
type: 'route' as const,
routeName: tool.route_name, // addUserTool
isDefault: false,
fromMarketplace: true,
marketplaceId: marketplaceId, // ID
toolName: toolName, // tool_name
componentPath: `tools/${toolName}/${toolName}.vue` // 使 tool_name
}
// store
toolsStore.addUserTool(toolData, router, toolData.componentPath)
}
//
loadInstalledTools()
//
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event('installedToolsUpdated'))
}
setTimeout(() => {
showProgressModal.value = false
}, 2000)
} else {
throw new Error(response.data.error || t('安装失败'))
}
} catch (error: any) {
console.error('Failed to install tool:', error)
message.error(error.response?.data?.detail || t('Failed to install tool'))
console.error('Install tool error:', error)
installing.value = false
installStatus.value = 'error'
installMessage.value = error.response?.data?.detail || error.message || t('安装失败')
message.error(error.response?.data?.detail || t('安装失败'))
setTimeout(() => {
showProgressModal.value = false
}, 3000)
}
}

View File

@ -235,6 +235,7 @@ def _install_tool_from_file(file_path: str) -> Dict[str, Any]:
'success': True,
'tool_id': result.get('tool_id'),
'tool_name': result.get('tool_name'),
'tool_title': result.get('tool_title'),
'message': f"工具 {result.get('tool_name')} 安装成功"
}
else:
@ -325,11 +326,29 @@ def _install_single_tool_directory(tool_dir: str) -> Dict[str, Any]:
if backend_source.exists() and backend_source.is_dir():
backend_target = jingrow_root / "tools" / tool_name
backend_target.mkdir(parents=True, exist_ok=True)
# 如果目录已存在,先删除
if backend_target.exists():
shutil.rmtree(backend_target)
shutil.copytree(backend_source, backend_target)
logger.info(f"复制后端文件: {backend_source} -> {backend_target}")
# 如果后端目录下有子目录(如 backend/{tool_name}/),复制子目录的内容
# 否则直接复制 backend/ 下的所有文件
subdirs = [d for d in backend_source.iterdir() if d.is_dir()]
if subdirs:
# 有子目录,复制第一个子目录的内容
source_subdir = subdirs[0]
if backend_target.exists():
shutil.rmtree(backend_target)
shutil.copytree(source_subdir, backend_target)
logger.info(f"复制后端文件目录: {source_subdir} -> {backend_target}")
else:
# 没有子目录,直接复制所有文件
if backend_target.exists():
shutil.rmtree(backend_target)
backend_target.mkdir(parents=True, exist_ok=True)
for item in backend_source.iterdir():
if item.is_file():
shutil.copy2(item, backend_target / item.name)
logger.info(f"复制后端文件: {item.name} -> {backend_target}")
elif item.is_dir():
shutil.copytree(item, backend_target / item.name)
logger.info(f"复制后端文件目录: {item.name} -> {backend_target}")
return {
'success': True,
@ -342,3 +361,62 @@ def _install_single_tool_directory(tool_dir: str) -> Dict[str, Any]:
logger.error(f"安装工具目录失败: {str(e)}")
return {'success': False, 'error': str(e)}
@router.post("/jingrow/uninstall-tool/{tool_name}")
async def uninstall_tool(tool_name: str):
"""卸载工具 - 删除前后端对应的文件夹"""
try:
logger.info(f"开始卸载工具: {tool_name}")
jingrow_root = get_jingrow_root()
# 前端目录apps/jingrow/frontend/src/views/tools/{tool_name}
frontend_root = jingrow_root.parent / "frontend" / "src"
tool_frontend_dir = frontend_root / "views" / "tools" / tool_name
# 后端目录apps/jingrow/jingrow/tools/{tool_name}
tool_backend_dir = jingrow_root / "tools" / tool_name
logger.info(f"前端目录路径: {tool_frontend_dir}")
logger.info(f"后端目录路径: {tool_backend_dir}")
logger.info(f"前端目录存在: {tool_frontend_dir.exists()}")
logger.info(f"后端目录存在: {tool_backend_dir.exists()}")
deleted_paths = []
# 删除前端目录
if tool_frontend_dir.exists():
shutil.rmtree(tool_frontend_dir)
deleted_paths.append(f"前端: {tool_frontend_dir}")
logger.info(f"删除工具前端目录成功: {tool_frontend_dir}")
else:
logger.warning(f"前端目录不存在: {tool_frontend_dir}")
# 删除后端目录
if tool_backend_dir.exists():
shutil.rmtree(tool_backend_dir)
deleted_paths.append(f"后端: {tool_backend_dir}")
logger.info(f"删除工具后端目录成功: {tool_backend_dir}")
else:
logger.warning(f"后端目录不存在: {tool_backend_dir}")
if not deleted_paths:
logger.warning(f"工具 {tool_name} 的文件目录不存在")
return {
'success': False,
'error': f'工具 {tool_name} 的文件目录不存在',
'frontend_path': str(tool_frontend_dir),
'backend_path': str(tool_backend_dir)
}
logger.info(f"工具 {tool_name} 卸载成功,已删除路径: {deleted_paths}")
return {
'success': True,
'message': f'工具 {tool_name} 卸载成功',
'deleted_paths': deleted_paths
}
except Exception as e:
logger.error(f"卸载工具失败: {str(e)}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"卸载工具失败: {str(e)}")