From 2c030be3318979c544c4c80650f0e006ddb8ba6f Mon Sep 17 00:00:00 2001 From: jingrow Date: Fri, 21 Nov 2025 12:32:45 +0800 Subject: [PATCH] reorganize tool marketplace architecture --- apps/jingrow/frontend/src/app/router/index.ts | 12 +- .../frontend/src/shared/stores/menu.ts | 5 +- .../frontend/src/shared/stores/tools.ts | 7 + .../frontend/src/views/{tools => }/Tools.vue | 56 +- .../frontend/src/views/dev/ToolDetail.vue | 521 +++++++++++++++++ .../src/views/dev/ToolMarketplace.vue | 522 ++++++++++++++++++ .../src/views/settings/MenuManager.vue | 4 +- apps/jingrow/jingrow/api/tools.py | 106 ++++ 8 files changed, 1220 insertions(+), 13 deletions(-) rename apps/jingrow/frontend/src/views/{tools => }/Tools.vue (95%) create mode 100644 apps/jingrow/frontend/src/views/dev/ToolDetail.vue create mode 100644 apps/jingrow/frontend/src/views/dev/ToolMarketplace.vue create mode 100644 apps/jingrow/jingrow/api/tools.py diff --git a/apps/jingrow/frontend/src/app/router/index.ts b/apps/jingrow/frontend/src/app/router/index.ts index 7e82b34..0c88a54 100644 --- a/apps/jingrow/frontend/src/app/router/index.ts +++ b/apps/jingrow/frontend/src/app/router/index.ts @@ -87,7 +87,7 @@ const router = createRouter({ { path: 'tools', name: 'Tools', - component: () => import('../../views/tools/Tools.vue') + component: () => import('../../views/Tools.vue') }, { path: 'tools/remove-background', @@ -145,6 +145,16 @@ const router = createRouter({ name: 'AgentDetail', component: () => import('../../views/dev/AgentDetail.vue') }, + { + path: 'tool-marketplace', + name: 'ToolMarketplace', + component: () => import('../../views/dev/ToolMarketplace.vue') + }, + { + path: 'tool-marketplace/:name', + name: 'ToolDetail', + component: () => import('../../views/dev/ToolDetail.vue') + }, { path: 'app-marketplace/:name', name: 'AppDetail', diff --git a/apps/jingrow/frontend/src/shared/stores/menu.ts b/apps/jingrow/frontend/src/shared/stores/menu.ts index bced554..b0a2488 100644 --- a/apps/jingrow/frontend/src/shared/stores/menu.ts +++ b/apps/jingrow/frontend/src/shared/stores/menu.ts @@ -68,6 +68,7 @@ function getDefaultMenus(): AppMenuItem[] { { id: 'app-marketplace', key: 'AppMarketplace', label: 'App Marketplace', icon: 'tabler:shopping-cart', type: 'route', routeName: 'AppMarketplace', parentId: 'dev-group', order: 7.5 }, { id: 'node-marketplace', key: 'NodeMarketplace', label: 'Node Marketplace', icon: 'carbon:add-child-node', type: 'route', routeName: 'NodeMarketplace', parentId: 'dev-group', order: 8 }, { id: 'agent-marketplace', key: 'AgentMarketplace', label: 'Agent Marketplace', icon: 'hugeicons:robotic', type: 'route', routeName: 'AgentMarketplace', parentId: 'dev-group', order: 8.5 }, + { id: 'tool-marketplace', key: 'ToolMarketplace', label: 'Tool Marketplace', icon: 'tabler:tool', type: 'route', routeName: 'ToolMarketplace', parentId: 'dev-group', order: 8.7 }, { id: 'menuManager', key: 'MenuManager', label: 'Menu Management', icon: 'tabler:menu-2', type: 'route', routeName: 'MenuManager', order: 11 }, { id: 'settings', key: 'Settings', label: 'Settings', icon: 'tabler:settings', routeName: 'Settings', order: 12, type: 'route' } ] @@ -189,9 +190,9 @@ export const useMenuStore = defineStore('menu', () => { return false } - // 开发分组下只允许显示:应用市场、节点市场、智能体市场 + // 开发分组下只允许显示:应用市场、节点市场、智能体市场、工具市场 if (m.parentId === 'dev-group') { - const allowedDevMenus = ['app-marketplace', 'node-marketplace', 'agent-marketplace'] + const allowedDevMenus = ['app-marketplace', 'node-marketplace', 'agent-marketplace', 'tool-marketplace'] if (!allowedDevMenus.includes(m.id)) { return false } diff --git a/apps/jingrow/frontend/src/shared/stores/tools.ts b/apps/jingrow/frontend/src/shared/stores/tools.ts index 814ce57..2b6bf83 100644 --- a/apps/jingrow/frontend/src/shared/stores/tools.ts +++ b/apps/jingrow/frontend/src/shared/stores/tools.ts @@ -15,6 +15,13 @@ export interface Tool { order?: number isDefault?: boolean hidden?: boolean + // 市场工具相关 + fromMarketplace?: boolean + marketplaceId?: string + version?: string + author?: string + rating?: number + downloads?: number } const STORAGE_KEY = 'tools.userItems' diff --git a/apps/jingrow/frontend/src/views/tools/Tools.vue b/apps/jingrow/frontend/src/views/Tools.vue similarity index 95% rename from apps/jingrow/frontend/src/views/tools/Tools.vue rename to apps/jingrow/frontend/src/views/Tools.vue index ff6d898..c51da8b 100644 --- a/apps/jingrow/frontend/src/views/tools/Tools.vue +++ b/apps/jingrow/frontend/src/views/Tools.vue @@ -2,10 +2,16 @@
@@ -172,10 +178,10 @@ import { ref, computed, onMounted, h } from 'vue' import { useRouter } from 'vue-router' import { NModal, NForm, NFormItem, NInput, NSelect, NAutoComplete, NColorPicker, NButton, NSpace, NText, NDropdown, useDialog, useMessage, type FormInst, type FormRules, type DropdownOption } from 'naive-ui' -import { t } from '../../shared/i18n' -import DynamicIcon from '../../core/components/DynamicIcon.vue' -import IconPicker from '../../core/components/IconPicker.vue' -import { useToolsStore, type Tool } from '../../shared/stores/tools' +import { t } from '@/shared/i18n' +import DynamicIcon from '@/core/components/DynamicIcon.vue' +import IconPicker from '@/core/components/IconPicker.vue' +import { useToolsStore, type Tool } from '@/shared/stores/tools' // 生成 UUID(兼容所有环境) function generateUUID(): string { @@ -481,6 +487,10 @@ function handleShowDefaultTool(toolId: string) { message.success(t('Tool shown successfully')) } +function handleOpenMarketplace() { + router.push({ name: 'ToolMarketplace' }) +} + function handleOpenTool(tool: Tool) { if (tool.type === 'route' && tool.routeName) { // 使用路由名导航(最佳实践) @@ -588,6 +598,36 @@ function handleMenuSelect(key: string, tool: Tool) { margin: 0; } +.header-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.marketplace-btn { + height: 36px; + padding: 0 16px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: white; + color: #64748b; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.marketplace-btn:hover { + background: #f9fafb; + border-color: #cbd5e1; + color: #475569; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + .add-tool-btn { height: 36px; padding: 0 16px; diff --git a/apps/jingrow/frontend/src/views/dev/ToolDetail.vue b/apps/jingrow/frontend/src/views/dev/ToolDetail.vue new file mode 100644 index 0000000..e82df91 --- /dev/null +++ b/apps/jingrow/frontend/src/views/dev/ToolDetail.vue @@ -0,0 +1,521 @@ + + + + + + diff --git a/apps/jingrow/frontend/src/views/dev/ToolMarketplace.vue b/apps/jingrow/frontend/src/views/dev/ToolMarketplace.vue new file mode 100644 index 0000000..76a7a02 --- /dev/null +++ b/apps/jingrow/frontend/src/views/dev/ToolMarketplace.vue @@ -0,0 +1,522 @@ + + + + + + diff --git a/apps/jingrow/frontend/src/views/settings/MenuManager.vue b/apps/jingrow/frontend/src/views/settings/MenuManager.vue index ee8b160..a4de1cd 100644 --- a/apps/jingrow/frontend/src/views/settings/MenuManager.vue +++ b/apps/jingrow/frontend/src/views/settings/MenuManager.vue @@ -180,9 +180,9 @@ const filteredItems = computed(() => { return false } - // 开发分组下只允许显示:应用市场、节点市场、智能体市场 + // 开发分组下只允许显示:应用市场、节点市场、智能体市场、工具市场 if (m.parentId === 'dev-group') { - const allowedDevMenus = ['app-marketplace', 'node-marketplace', 'agent-marketplace'] + const allowedDevMenus = ['app-marketplace', 'node-marketplace', 'agent-marketplace', 'tool-marketplace'] if (!allowedDevMenus.includes(m.id)) { return false } diff --git a/apps/jingrow/jingrow/api/tools.py b/apps/jingrow/jingrow/api/tools.py new file mode 100644 index 0000000..d938a86 --- /dev/null +++ b/apps/jingrow/jingrow/api/tools.py @@ -0,0 +1,106 @@ +# Copyright (c) 2025, JINGROW and contributors +# For license information, please see license.txt + +""" +Jingrow Tools API +工具相关的 FastAPI 路由 +""" + +from fastapi import APIRouter, HTTPException +from typing import Dict, Any, Optional +import json +import requests +import logging + +from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/jingrow/tool-marketplace") +async def get_tool_marketplace( + search: Optional[str] = None, + page: int = 1, + page_size: int = 20, + sort_by: Optional[str] = None +): + """获取工具市场数据,支持搜索、分页和排序""" + try: + url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.get_local_tool_list" + + # 构建过滤条件 + filters = {"public": 1} + if search: + filters["name"] = ["like", f"%{search}%"] + filters["title"] = ["like", f"%{search}%"] + filters["description"] = ["like", f"%{search}%"] + + # 1. 先获取总数(不分页) + total_params = { + 'filters': json.dumps(filters, ensure_ascii=False), + 'limit_start': 0, + 'limit_page_length': 0 + } + + headers = get_jingrow_cloud_api_headers() + total_response = requests.get(url, params=total_params, headers=headers, timeout=20) + + total_count = 0 + if total_response.status_code == 200: + total_data = total_response.json() + total_count = len(total_data.get('message', [])) + + # 2. 获取分页数据 + params = { + 'filters': json.dumps(filters, ensure_ascii=False) + } + + # 排序参数 + if sort_by: + params['order_by'] = sort_by + + # 分页参数 + limit_start = (page - 1) * page_size + params['limit_start'] = limit_start + params['limit_page_length'] = page_size + + response = requests.get(url, params=params, headers=headers, timeout=20) + + if response.status_code == 200: + data = response.json() + tools = data.get('message', []) + + return { + "items": tools, + "total": total_count, + "page": page, + "page_size": page_size + } + else: + raise HTTPException(status_code=response.status_code, detail="获取工具市场数据失败") + + except Exception as e: + logger.error(f"获取工具市场数据失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取工具市场数据失败: {str(e)}") + + +@router.get("/jingrow/tool-marketplace/{name}") +async def get_tool_detail(name: str): + """获取工具详情""" + try: + url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.get_local_tool" + params = {"name": name} + + headers = get_jingrow_cloud_api_headers() + response = requests.get(url, params=params, headers=headers, timeout=20) + + if response.status_code == 200: + data = response.json() + return data.get('message') + else: + raise HTTPException(status_code=404, detail="工具不存在") + except Exception as e: + logger.error(f"获取工具详情失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取工具详情失败: {str(e)}") +