节点详情页工具栏增加一键发布节点到节点市场的功能
This commit is contained in:
parent
4dcfcf3e10
commit
c26391d874
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -61,6 +61,23 @@
|
||||
{{ t('Edit Schema') }}
|
||||
</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 编辑器模态框 -->
|
||||
<SchemaEditorModal
|
||||
v-model:visible="showSchemaEditor"
|
||||
@ -106,6 +123,8 @@ import { t } from '@/shared/i18n'
|
||||
import SchemaEditorModal from '@/core/components/SchemaEditorModal.vue'
|
||||
import axios from 'axios'
|
||||
import { get_session_api_headers } from '@/shared/api/auth'
|
||||
import { packageNode, publishNodeToMarketplace } from '@/shared/api/nodes'
|
||||
import { uploadFileToJingrow } from '@/shared/api/common'
|
||||
|
||||
interface Props {
|
||||
entity: string
|
||||
@ -144,6 +163,9 @@ const showSchemaEditor = ref(false)
|
||||
const nodeName = computed(() => 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;
|
||||
|
||||
@ -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)}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user