节点详情页工具栏增加一键发布节点到节点市场的功能

This commit is contained in:
jingrow 2025-11-03 01:24:29 +08:00
parent 4dcfcf3e10
commit c26391d874
3 changed files with 307 additions and 1 deletions

View File

@ -149,3 +149,60 @@ export const importLocalNodes = async (): Promise<{ success: boolean; matched: n
} }
} }
// 打包节点为zip文件
export const packageNode = async (nodeType: string): Promise<Blob> => {
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
}
}

View File

@ -61,6 +61,23 @@
{{ t('Edit Schema') }} {{ t('Edit Schema') }}
</n-button> </n-button>
<!-- 发布到节点市场按钮 -->
<n-button
type="default"
size="medium"
@click="handlePublishToMarketplace"
:disabled="loading || isNew || !nodeRecord.node_type"
:loading="publishing"
v-if="!isNew"
class="toolbar-btn publish-btn"
:title="t('Publish to Node Marketplace')"
>
<template #icon>
<n-icon><Icon icon="tabler:cloud-upload" /></n-icon>
</template>
{{ t('Publish to Marketplace') }}
</n-button>
<!-- Schema 编辑器模态框 --> <!-- Schema 编辑器模态框 -->
<SchemaEditorModal <SchemaEditorModal
v-model:visible="showSchemaEditor" v-model:visible="showSchemaEditor"
@ -106,6 +123,8 @@ import { t } from '@/shared/i18n'
import SchemaEditorModal from '@/core/components/SchemaEditorModal.vue' import SchemaEditorModal from '@/core/components/SchemaEditorModal.vue'
import axios from 'axios' import axios from 'axios'
import { get_session_api_headers } from '@/shared/api/auth' import { get_session_api_headers } from '@/shared/api/auth'
import { packageNode, publishNodeToMarketplace } from '@/shared/api/nodes'
import { uploadFileToJingrow } from '@/shared/api/common'
interface Props { interface Props {
entity: string entity: string
@ -144,6 +163,9 @@ const showSchemaEditor = ref(false)
const nodeName = computed(() => String(props.id)) const nodeName = computed(() => String(props.id))
const nodeRecord = computed(() => props.record || {}) const nodeRecord = computed(() => props.record || {})
//
const publishing = ref(false)
function openSchemaEditor() { function openSchemaEditor() {
if (isNew.value) return 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 // localStorage
const sidebarPosition = ref<'left' | 'right'>(props.sidebarPosition || 'left') const sidebarPosition = ref<'left' | 'right'>(props.sidebarPosition || 'left')
@ -341,6 +441,20 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(13, 104, 75, 0.15); 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 { .toolbar-btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;

View File

@ -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 typing import Dict, Any, List, Optional
from pathlib import Path from pathlib import Path
import json import json
@ -582,3 +582,138 @@ def _install_single_node_directory(node_dir: str) -> Dict[str, Any]:
except Exception as e: except Exception as e:
logger.error(f"安装节点目录失败: {str(e)}") logger.error(f"安装节点目录失败: {str(e)}")
return {'success': False, 'error': 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)}")