节点详情页工具栏增加一键发布节点到节点市场的功能
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') }}
|
{{ 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;
|
||||||
|
|||||||
@ -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)}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user