From c26391d8741715a2bf6c47f07142d3151167f5bc Mon Sep 17 00:00:00 2001 From: jingrow Date: Mon, 3 Nov 2025 01:24:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=A0=8F=E5=A2=9E=E5=8A=A0=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E8=8A=82=E7=82=B9=E5=88=B0=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E5=B8=82=E5=9C=BA=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jingrow/frontend/src/shared/api/nodes.ts | 57 ++++++++ .../local_ai_node/local_ai_node_toolbar.vue | 114 +++++++++++++++ apps/jingrow/jingrow/api/node_management.py | 137 +++++++++++++++++- 3 files changed, 307 insertions(+), 1 deletion(-) diff --git a/apps/jingrow/frontend/src/shared/api/nodes.ts b/apps/jingrow/frontend/src/shared/api/nodes.ts index 214ffd7..cf1a86b 100644 --- a/apps/jingrow/frontend/src/shared/api/nodes.ts +++ b/apps/jingrow/frontend/src/shared/api/nodes.ts @@ -149,3 +149,60 @@ export const importLocalNodes = async (): Promise<{ success: boolean; matched: n } } +// 打包节点为zip文件 +export const packageNode = async (nodeType: string): Promise => { + try { + const response = await axios.post( + `/jingrow/node/package/${encodeURIComponent(nodeType)}`, + {}, + { + headers: get_session_api_headers(), + withCredentials: true, + responseType: 'blob' + } + ) + return response.data + } catch (error) { + console.error('打包节点失败:', error) + throw error + } +} + +// 发布节点到节点市场 +export const publishNodeToMarketplace = async (data: { + node_type: string + title: string + subtitle?: string + description?: string + file_url: string + repository_url?: string + node_image?: string +}): Promise<{ success: boolean; message?: string }> => { + try { + const formData = new FormData() + formData.append('node_type', data.node_type) + formData.append('title', data.title) + if (data.subtitle) formData.append('subtitle', data.subtitle) + if (data.description) formData.append('description', data.description) + formData.append('file_url', data.file_url) + if (data.repository_url) formData.append('repository_url', data.repository_url) + if (data.node_image) formData.append('node_image', data.node_image) + + const response = await axios.post( + `/jingrow/node/publish`, + formData, + { + headers: { + ...get_session_api_headers(), + 'Content-Type': 'multipart/form-data' + }, + withCredentials: true + } + ) + return response.data + } catch (error: any) { + console.error('发布节点失败:', error) + throw error + } +} + diff --git a/apps/jingrow/frontend/src/views/pagetype/local_ai_node/local_ai_node_toolbar.vue b/apps/jingrow/frontend/src/views/pagetype/local_ai_node/local_ai_node_toolbar.vue index c04cb30..80bdf22 100644 --- a/apps/jingrow/frontend/src/views/pagetype/local_ai_node/local_ai_node_toolbar.vue +++ b/apps/jingrow/frontend/src/views/pagetype/local_ai_node/local_ai_node_toolbar.vue @@ -61,6 +61,23 @@ {{ t('Edit Schema') }} + + + + {{ t('Publish to Marketplace') }} + + String(props.id)) const nodeRecord = computed(() => props.record || {}) +// 发布到节点市场相关 +const publishing = ref(false) + function openSchemaEditor() { if (isNew.value) return @@ -186,6 +208,84 @@ async function handleSchemaSave(schemaData: any) { } } +async function handlePublishToMarketplace() { + if (isNew.value || !nodeRecord.value.node_type) { + message.warning(t('Please save the node first')) + return + } + + const nodeType = nodeRecord.value.node_type + const nodeLabel = nodeRecord.value.node_label || nodeType + const nodeDescription = nodeRecord.value.node_description || '' + + publishing.value = true + let loadingMessage: any = null + + try { + // 1. 打包节点为zip + loadingMessage = message.loading(t('Packaging node...'), { duration: 0 }) + + const zipBlob = await packageNode(nodeType) + const zipFile = new File([zipBlob], `${nodeType}.zip`, { type: 'application/zip' }) + + // 更新消息 + if (loadingMessage) { + loadingMessage.destroy() + } + loadingMessage = message.loading(t('Uploading package...'), { duration: 0 }) + + // 2. 上传zip文件到服务器 + const uploadResult = await uploadFileToJingrow( + zipFile, + 'Local Ai Node', + nodeName.value, + 'file_url' + ) + + if (!uploadResult.success || !uploadResult.file_url) { + throw new Error(uploadResult.error || t('Upload failed')) + } + + const fileUrl = uploadResult.file_url + + // 更新消息 + if (loadingMessage) { + loadingMessage.destroy() + } + loadingMessage = message.loading(t('Publishing to marketplace...'), { duration: 0 }) + + // 3. 发布到节点市场 + const publishResult = await publishNodeToMarketplace({ + node_type: nodeType, + title: nodeLabel, + description: nodeDescription, + file_url: fileUrl, + repository_url: nodeRecord.value.repository_url, + node_image: nodeRecord.value.node_image + }) + + if (loadingMessage) { + loadingMessage.destroy() + } + + if (publishResult.success) { + message.success(t('Node published to marketplace successfully')) + } else { + throw new Error(publishResult.message || t('Publish failed')) + } + + } catch (error: any) { + if (loadingMessage) { + loadingMessage.destroy() + } + console.error('发布节点失败:', error) + const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || t('Publish failed, please check permission and server logs') + message.error(errorMsg) + } finally { + publishing.value = false + } +} + // 从 localStorage 读取侧边栏位置(如果父组件没有传入) const sidebarPosition = ref<'left' | 'right'>(props.sidebarPosition || 'left') @@ -341,6 +441,20 @@ onMounted(() => { box-shadow: 0 2px 8px rgba(13, 104, 75, 0.15); } +/* 发布到市场按钮 - 蓝色系 */ +.publish-btn { + background: #eff6ff !important; + color: #1e40af !important; + border-color: rgba(30, 64, 175, 0.2) !important; +} + +.publish-btn:hover:not(:disabled) { + background: #dbeafe !important; + color: #1e3a8a !important; + border-color: rgba(30, 64, 175, 0.3) !important; + box-shadow: 0 2px 8px rgba(30, 64, 175, 0.15); +} + .toolbar-btn:disabled { opacity: 0.5; cursor: not-allowed; diff --git a/apps/jingrow/jingrow/api/node_management.py b/apps/jingrow/jingrow/api/node_management.py index 525053b..7bcb59d 100644 --- a/apps/jingrow/jingrow/api/node_management.py +++ b/apps/jingrow/jingrow/api/node_management.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Form +from fastapi import APIRouter, HTTPException, Form, Response from typing import Dict, Any, List, Optional from pathlib import Path import json @@ -582,3 +582,138 @@ def _install_single_node_directory(node_dir: str) -> Dict[str, Any]: except Exception as e: logger.error(f"安装节点目录失败: {str(e)}") return {'success': False, 'error': str(e)} + + +@router.post("/jingrow/node/package/{node_type}") +async def package_node(node_type: str): + """ + 打包节点文件夹为zip文件,返回文件路径用于后续上传 + """ + try: + from datetime import datetime + + current_file = Path(__file__).resolve() + jingrow_root = current_file.parents[1] + nodes_root = jingrow_root / "ai" / "nodes" + node_dir = nodes_root / node_type + + if not node_dir.exists(): + raise HTTPException(status_code=404, detail=f"节点目录不存在: {node_type}") + + # 创建临时打包目录 + root = current_file.parents[4] + tmp_dir = root / "tmp" + tmp_dir.mkdir(parents=True, exist_ok=True) + + # 创建临时目录用于打包 + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + temp_package_dir = tmp_dir / f"{node_type}-{timestamp}" + temp_package_dir.mkdir(parents=True, exist_ok=True) + + try: + # 复制节点目录内容(排除不必要的文件) + for item in node_dir.iterdir(): + if item.name in ['__pycache__', '.git', '.DS_Store', '.pytest_cache']: + continue + dst = temp_package_dir / item.name + if item.is_dir(): + shutil.copytree(item, dst, dirs_exist_ok=True) + else: + shutil.copy2(item, dst) + + # 打包为 ZIP + zip_filename = f"{node_type}-{timestamp}.zip" + zip_base_name = tmp_dir / f"{node_type}-{timestamp}" + shutil.make_archive(str(zip_base_name), 'zip', root_dir=str(tmp_dir), base_dir=f"{node_type}-{timestamp}") + + zip_path = tmp_dir / f"{zip_filename}" + + if not zip_path.exists(): + raise HTTPException(status_code=500, detail="ZIP文件创建失败") + + # 读取文件内容 + with open(zip_path, 'rb') as f: + zip_content = f.read() + + # 清理临时文件 + if zip_path.exists(): + os.remove(zip_path) + + # 返回文件内容,前端可以直接使用 + return Response( + content=zip_content, + media_type="application/zip", + headers={ + "Content-Disposition": f"attachment; filename={zip_filename}", + "Content-Type": "application/zip" + } + ) + + finally: + # 清理临时目录 + if temp_package_dir.exists(): + shutil.rmtree(temp_package_dir, ignore_errors=True) + + except HTTPException: + raise + 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)}") + + +@router.post("/jingrow/node/publish") +async def publish_node_to_marketplace( + node_type: str = Form(...), + title: str = Form(...), + subtitle: Optional[str] = Form(None), + description: Optional[str] = Form(None), + file_url: str = Form(...), + repository_url: Optional[str] = Form(None), + node_image: Optional[str] = Form(None) +): + """ + 发布节点到Jingrow Cloud节点市场 + """ + try: + url = f"{get_jingrow_cloud_url()}/api/action/jcloud.api.jlocal.create_local_node" + + headers = get_jingrow_cloud_api_headers() + headers['Content-Type'] = 'application/json' + + response = requests.post(url, json={ + "node_data": { + "node_type": node_type, + "title": title, + "subtitle": subtitle or "", + "description": description or "", + "file_url": file_url, + "repository_url": repository_url, + "node_image": node_image + } + }, headers=headers, timeout=20) + + if response.status_code != 200: + error_detail = response.json().get('detail', f"HTTP {response.status_code}") if response.headers.get('content-type', '').startswith('application/json') else f"HTTP {response.status_code}" + raise HTTPException(status_code=response.status_code, detail=error_detail) + + result = response.json() + + # 检查错误 + if isinstance(result, dict) and result.get('error'): + raise HTTPException(status_code=400, detail=result['error']) + + message = result.get('message', {}) + if isinstance(message, dict) and message.get('error'): + raise HTTPException(status_code=400, detail=message['error']) + + # 成功响应 + node_name = message.get('name', 'unknown') if isinstance(message, dict) else result.get('message', 'unknown') + return {"success": True, "message": f"节点发布成功,节点名称: {node_name}"} + + except HTTPException: + raise + 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)}")