优化node_management.py

This commit is contained in:
jingrow 2025-11-03 03:12:22 +08:00
parent d348a656c0
commit 5a758b3a8f

View File

@ -13,6 +13,7 @@ from jingrow.utils.fs import atomic_write_json
from jingrow.utils.jingrow_api import get_record_id, create_record, update_record, get_record_list from jingrow.utils.jingrow_api import get_record_id, create_record, update_record, get_record_list
from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers from jingrow.utils.auth import get_jingrow_cloud_url, get_jingrow_cloud_api_headers
from jingrow.config import Config from jingrow.config import Config
from jingrow.utils.path import get_root_path, get_apps_path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,8 +34,7 @@ async def export_node_definition(payload: Dict[str, Any]):
export_data = {"metadata": metadata, **(schema or {})} export_data = {"metadata": metadata, **(schema or {})}
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
jingrow_root = current_file.parents[1] # 修正路径层级
new_root = jingrow_root / "ai" / "nodes" new_root = jingrow_root / "ai" / "nodes"
target = new_root / node_type / f"{node_type}.json" target = new_root / node_type / f"{node_type}.json"
atomic_write_json(target, export_data) atomic_write_json(target, export_data)
@ -49,8 +49,7 @@ async def import_local_node_definitions():
扫描本地节点定义目录 metadata 去重后导入到 Local Ai Node 扫描本地节点定义目录 metadata 去重后导入到 Local Ai Node
""" """
try: try:
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
jingrow_root = current_file.parents[1] # 修正路径层级
nodes_root = jingrow_root / "ai" / "nodes" nodes_root = jingrow_root / "ai" / "nodes"
if not nodes_root.exists(): if not nodes_root.exists():
return {"success": True, "matched": 0, "imported": 0, "skipped_existing": 0} return {"success": True, "matched": 0, "imported": 0, "skipped_existing": 0}
@ -128,8 +127,7 @@ async def get_all_node_metadata():
获取所有节点的元数据用于流程编排界面 获取所有节点的元数据用于流程编排界面
""" """
try: try:
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
jingrow_root = current_file.parents[1] # 修正路径层级
nodes_root = jingrow_root / "ai" / "nodes" nodes_root = jingrow_root / "ai" / "nodes"
if not nodes_root.exists(): if not nodes_root.exists():
@ -177,8 +175,7 @@ async def get_node_schema(node_type: str):
获取指定节点类型的Schema配置 获取指定节点类型的Schema配置
""" """
try: try:
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
jingrow_root = current_file.parents[1]
nodes_root = jingrow_root / "ai" / "nodes" nodes_root = jingrow_root / "ai" / "nodes"
json_file = nodes_root / node_type / f"{node_type}.json" json_file = nodes_root / node_type / f"{node_type}.json"
@ -303,151 +300,148 @@ async def check_node_exists(node_type: str):
@router.get("/jingrow/installed-node-types") @router.get("/jingrow/installed-node-types")
async def get_installed_node_types(): async def get_installed_node_types():
""" """
获取已安装的节点类型列表 获取已安装的节点类型列表
通过扫描节点目录获取这是最高效的方式只需要列出目录名不需要读取文件 通过扫描节点目录获取这是最高效的方式只需要列出目录名不需要读取文件
""" """
try: try:
# 确定节点目录路径apps/jingrow/jingrow/ai/nodes # 确定节点目录路径apps/jingrow/jingrow/ai/nodes
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
jingrow_root = current_file.parents[1] # jingrow nodes_root = jingrow_root / "ai" / "nodes"
nodes_root = jingrow_root / "ai" / "nodes"
node_types = []
node_types = []
# 如果目录不存在,返回空列表
# 如果目录不存在,返回空列表 if not nodes_root.exists():
if not nodes_root.exists(): return {"success": True, "node_types": []}
return {"success": True, "node_types": []}
# 扫描目录,只获取目录名(最高效的方式)
# 扫描目录,只获取目录名(最高效的方式) for item in nodes_root.iterdir():
for item in nodes_root.iterdir(): if item.is_dir() and not item.name.startswith('.') and item.name != '__pycache__':
if item.is_dir() and not item.name.startswith('.') and item.name != '__pycache__': # 验证是否包含节点定义文件(可选,但更可靠)
# 验证是否包含节点定义文件(可选,但更可靠) json_file = item / f"{item.name}.json"
json_file = item / f"{item.name}.json" if json_file.exists():
if json_file.exists(): node_types.append(item.name)
node_types.append(item.name)
return {"success": True, "node_types": sorted(node_types)}
return {"success": True, "node_types": sorted(node_types)} except Exception as e:
except Exception as e: logger.error(f"获取已安装节点类型失败: {str(e)}")
logger.error(f"获取已安装节点类型失败: {str(e)}") logger.error(f"Traceback: {traceback.format_exc()}")
logger.error(f"Traceback: {traceback.format_exc()}") return {"success": True, "node_types": []}
return {"success": True, "node_types": []}
@router.post("/jingrow/install-node-from-url") @router.post("/jingrow/install-node-from-url")
async def install_node_from_url(url: str = Form(...)): async def install_node_from_url(url: str = Form(...)):
"""从URL安装节点""" """从URL安装节点"""
try: try:
# 下载文件 # 下载文件
current = Path(__file__).resolve() root = get_root_path()
root = current.parents[4] tmp_dir = root / "tmp"
tmp_dir = root / "tmp" tmp_dir.mkdir(parents=True, exist_ok=True)
tmp_dir.mkdir(parents=True, exist_ok=True)
# 创建临时文件
# 创建临时文件 temp_filename = f"node_download_{uuid.uuid4().hex[:8]}{Path(url).suffix}"
temp_filename = f"node_download_{uuid.uuid4().hex[:8]}{Path(url).suffix}" temp_file_path = tmp_dir / temp_filename
temp_file_path = tmp_dir / temp_filename
# 下载文件
# 下载文件 response = requests.get(url, stream=True, timeout=300)
response = requests.get(url, stream=True, timeout=300) response.raise_for_status()
response.raise_for_status()
with open(temp_file_path, 'wb') as f:
with open(temp_file_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192):
for chunk in response.iter_content(chunk_size=8192): f.write(chunk)
f.write(chunk)
# 安装节点
# 安装节点 result = _install_node_from_file(str(temp_file_path))
result = _install_node_from_file(str(temp_file_path))
# 清理临时文件
# 清理临时文件 if temp_file_path.exists():
if temp_file_path.exists(): os.remove(temp_file_path)
os.remove(temp_file_path)
return result
return result
except Exception as e:
except Exception as e: logger.error(f"从URL安装节点失败: {str(e)}")
logger.error(f"从URL安装节点失败: {str(e)}") logger.error(f"Traceback: {traceback.format_exc()}")
logger.error(f"Traceback: {traceback.format_exc()}") raise HTTPException(status_code=500, detail=f"安装节点失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"安装节点失败: {str(e)}")
@router.post("/jingrow/install-node-from-git") @router.post("/jingrow/install-node-from-git")
async def install_node_from_git(repo_url: str = Form(...)): async def install_node_from_git(repo_url: str = Form(...)):
"""从git仓库克隆并安装节点""" """从git仓库克隆并安装节点"""
try: try:
current = Path(__file__).resolve() root = get_root_path()
root = current.parents[4] tmp_dir = root / "tmp"
tmp_dir = root / "tmp" tmp_dir.mkdir(parents=True, exist_ok=True)
tmp_dir.mkdir(parents=True, exist_ok=True)
# 创建临时目录用于克隆
# 创建临时目录用于克隆 clone_dir = tmp_dir / f"node_git_clone_{uuid.uuid4().hex[:8]}"
clone_dir = tmp_dir / f"node_git_clone_{uuid.uuid4().hex[:8]}"
try:
try: # 使用 git clone 克隆仓库
# 使用 git clone 克隆仓库 result = subprocess.run(
result = subprocess.run( ['git', 'clone', repo_url, str(clone_dir)],
['git', 'clone', repo_url, str(clone_dir)], capture_output=True,
capture_output=True, text=True,
text=True, timeout=300
timeout=300 )
)
if result.returncode != 0:
if result.returncode != 0: raise HTTPException(status_code=400, detail=f"Git 克隆失败: {result.stderr}")
raise HTTPException(status_code=400, detail=f"Git 克隆失败: {result.stderr}")
# 查找节点目录(节点包应该包含一个或多个节点目录)
# 查找节点目录(节点包应该包含一个或多个节点目录) node_dirs = []
node_dirs = [] for item in clone_dir.iterdir():
for item in clone_dir.iterdir(): if item.is_dir() and not item.name.startswith('.') and item.name != '__pycache__':
if item.is_dir() and not item.name.startswith('.') and item.name != '__pycache__': json_file = item / f"{item.name}.json"
json_file = item / f"{item.name}.json" if json_file.exists():
if json_file.exists(): node_dirs.append(item)
node_dirs.append(item)
if not node_dirs:
if not node_dirs: raise HTTPException(status_code=400, detail="仓库中没有找到节点定义文件")
raise HTTPException(status_code=400, detail="仓库中没有找到节点定义文件")
# 安装所有找到的节点
# 安装所有找到的节点 installed_nodes = []
installed_nodes = [] errors = []
errors = []
for node_dir in node_dirs:
for node_dir in node_dirs: try:
try: result = _install_single_node_directory(str(node_dir))
result = _install_single_node_directory(str(node_dir)) if result.get('success'):
if result.get('success'): installed_nodes.append(node_dir.name)
installed_nodes.append(node_dir.name) else:
else: errors.append(f"{node_dir.name}: {result.get('error')}")
errors.append(f"{node_dir.name}: {result.get('error')}") except Exception as e:
except Exception as e: errors.append(f"{node_dir.name}: {str(e)}")
errors.append(f"{node_dir.name}: {str(e)}")
# 清理临时目录
# 清理临时目录 shutil.rmtree(clone_dir, ignore_errors=True)
shutil.rmtree(clone_dir, ignore_errors=True)
if errors:
if errors: return {
return { 'success': len(installed_nodes) > 0,
'success': len(installed_nodes) > 0, 'installed': installed_nodes,
'installed': installed_nodes, 'errors': errors,
'errors': errors, 'message': f"成功安装 {len(installed_nodes)} 个节点,失败 {len(errors)}"
'message': f"成功安装 {len(installed_nodes)} 个节点,失败 {len(errors)}" }
} else:
else: return {
return { 'success': True,
'success': True, 'installed': installed_nodes,
'installed': installed_nodes, 'message': f"成功安装 {len(installed_nodes)} 个节点"
'message': f"成功安装 {len(installed_nodes)} 个节点" }
}
finally:
finally: # 确保清理临时目录
# 确保清理临时目录 if clone_dir.exists():
if clone_dir.exists(): shutil.rmtree(clone_dir, ignore_errors=True)
shutil.rmtree(clone_dir, ignore_errors=True)
except HTTPException:
except HTTPException: raise
raise except Exception as e:
except Exception as e: logger.error(f"从Git安装节点失败: {str(e)}")
logger.error(f"从Git安装节点失败: {str(e)}") logger.error(f"Traceback: {traceback.format_exc()}")
logger.error(f"Traceback: {traceback.format_exc()}") raise HTTPException(status_code=500, detail=f"安装节点失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"安装节点失败: {str(e)}")
def _install_node_from_file(file_path: str) -> Dict[str, Any]: def _install_node_from_file(file_path: str) -> Dict[str, Any]:
@ -511,165 +505,161 @@ def _install_node_from_file(file_path: str) -> Dict[str, Any]:
def _install_single_node_directory(node_dir: str) -> Dict[str, Any]: def _install_single_node_directory(node_dir: str) -> Dict[str, Any]:
"""安装单个节点目录到 ai/nodes 并导入数据库""" """安装单个节点目录到 ai/nodes 并导入数据库"""
try: try:
node_dir_path = Path(node_dir) node_dir_path = Path(node_dir)
node_name = node_dir_path.name node_name = node_dir_path.name
# 读取节点定义文件 # 读取节点定义文件
json_file = node_dir_path / f"{node_name}.json" json_file = node_dir_path / f"{node_name}.json"
if not json_file.exists(): if not json_file.exists():
return {'success': False, 'error': f'找不到节点定义文件: {json_file.name}'} return {'success': False, 'error': f'找不到节点定义文件: {json_file.name}'}
with open(json_file, 'r', encoding='utf-8') as f: with open(json_file, 'r', encoding='utf-8') as f:
node_data = json.load(f) node_data = json.load(f)
if not isinstance(node_data, dict): if not isinstance(node_data, dict):
return {'success': False, 'error': '节点定义文件格式错误'} return {'success': False, 'error': '节点定义文件格式错误'}
metadata = node_data.get("metadata") or {} metadata = node_data.get("metadata") or {}
node_type = metadata.get("type") node_type = metadata.get("type")
if not node_type: if not node_type:
return {'success': False, 'error': '节点定义中缺少 metadata.type'} return {'success': False, 'error': '节点定义中缺少 metadata.type'}
# 确定目标目录apps/jingrow/jingrow/ai/nodes # 确定目标目录apps/jingrow/jingrow/ai/nodes
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
# node_management.py 位于 jingrow/api/ nodes_root = jingrow_root / "ai" / "nodes"
# parents[0] = jingrow/api, parents[1] = jingrow nodes_root.mkdir(parents=True, exist_ok=True)
jingrow_root = current_file.parents[1] # jingrow
nodes_root = jingrow_root / "ai" / "nodes" target_node_dir = nodes_root / node_type
nodes_root.mkdir(parents=True, exist_ok=True)
# 如果目标目录已存在,先删除
target_node_dir = nodes_root / node_type if target_node_dir.exists():
shutil.rmtree(target_node_dir)
# 如果目标目录已存在,先删除
if target_node_dir.exists(): # 复制整个节点目录
shutil.rmtree(target_node_dir) shutil.copytree(node_dir_path, target_node_dir)
# 复制整个节点目录 # 导入到数据库
shutil.copytree(node_dir_path, target_node_dir) # 检查是否已存在
exists_res = get_record_id(
# 导入到数据库 pagetype="Local Ai Node",
# 检查是否已存在 field="node_type",
exists_res = get_record_id( value=node_type,
pagetype="Local Ai Node", )
field="node_type",
value=node_type, # 生成 schema移除 metadata
) schema = dict(node_data)
schema.pop("metadata", None)
# 生成 schema移除 metadata
schema = dict(node_data) payload = {
schema.pop("metadata", None) "node_type": node_type,
"node_label": metadata.get("label") or node_type,
payload = { "node_icon": metadata.get("icon") or "fa-cube",
"node_type": node_type, "node_color": metadata.get("color") or "#6b7280",
"node_label": metadata.get("label") or node_type, "node_group": metadata.get("group") or "",
"node_icon": metadata.get("icon") or "fa-cube", "node_component": metadata.get("component_type") or "GenericNode",
"node_color": metadata.get("color") or "#6b7280", "node_description": metadata.get("description") or "",
"node_group": metadata.get("group") or "", "status": "Published",
"node_component": metadata.get("component_type") or "GenericNode", "node_schema": schema,
"node_description": metadata.get("description") or "", }
"status": "Published",
"node_schema": schema, if exists_res.get("success"):
} # 更新现有记录,使用 node_type 作为 name
res = update_record("Local Ai Node", node_type, payload)
if exists_res.get("success"): else:
# 更新现有记录,使用 node_type 作为 name # 创建新记录
res = update_record("Local Ai Node", node_type, payload) res = create_record("Local Ai Node", payload)
else:
# 创建新记录 if res.get("success"):
res = create_record("Local Ai Node", payload) return {'success': True, 'node_type': node_type, 'message': f'节点 {node_type} 安装成功'}
else:
if res.get("success"): return {'success': False, 'error': res.get('error', '导入数据库失败')}
return {'success': True, 'node_type': node_type, 'message': f'节点 {node_type} 安装成功'}
else: except Exception as e:
return {'success': False, 'error': res.get('error', '导入数据库失败')} logger.error(f"安装节点目录失败: {str(e)}")
return {'success': False, 'error': str(e)}
except Exception as e:
logger.error(f"安装节点目录失败: {str(e)}")
return {'success': False, 'error': str(e)}
@router.post("/jingrow/node/package/{node_type}") @router.post("/jingrow/node/package/{node_type}")
async def package_node(node_type: str): async def package_node(node_type: str):
""" """
打包节点文件夹为zip文件返回文件路径用于后续上传 打包节点文件夹为zip文件返回文件路径用于后续上传
""" """
try: try:
from datetime import datetime from datetime import datetime
current_file = Path(__file__).resolve() jingrow_root = get_apps_path() / "jingrow"
jingrow_root = current_file.parents[1] nodes_root = jingrow_root / "ai" / "nodes"
nodes_root = jingrow_root / "ai" / "nodes" node_dir = nodes_root / node_type
node_dir = nodes_root / node_type
if not node_dir.exists():
if not node_dir.exists(): raise HTTPException(status_code=404, detail=f"节点目录不存在: {node_type}")
raise HTTPException(status_code=404, detail=f"节点目录不存在: {node_type}")
# 创建临时打包目录
# 创建临时打包目录 root = get_root_path()
root = current_file.parents[4] tmp_dir = root / "tmp"
tmp_dir = root / "tmp" tmp_dir.mkdir(parents=True, exist_ok=True)
tmp_dir.mkdir(parents=True, exist_ok=True)
# 创建临时目录用于打包,文件夹名称保持为节点类型(不加时间戳)
# 创建临时目录用于打包,文件夹名称保持为节点类型(不加时间戳) timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
timestamp = datetime.now().strftime("%Y%m%d%H%M%S") temp_package_dir = tmp_dir / node_type
temp_package_dir = tmp_dir / node_type if temp_package_dir.exists():
if temp_package_dir.exists(): # 如果临时目录已存在,先删除
# 如果临时目录已存在,先删除 shutil.rmtree(temp_package_dir)
shutil.rmtree(temp_package_dir) temp_package_dir.mkdir(parents=True, exist_ok=True)
temp_package_dir.mkdir(parents=True, exist_ok=True)
try:
try: # 复制节点目录内容(排除不必要的文件)
# 复制节点目录内容(排除不必要的文件) for item in node_dir.iterdir():
for item in node_dir.iterdir(): if item.name in ['__pycache__', '.git', '.DS_Store', '.pytest_cache']:
if item.name in ['__pycache__', '.git', '.DS_Store', '.pytest_cache']: continue
continue dst = temp_package_dir / item.name
dst = temp_package_dir / item.name if item.is_dir():
if item.is_dir(): shutil.copytree(item, dst, dirs_exist_ok=True)
shutil.copytree(item, dst, dirs_exist_ok=True) else:
else: shutil.copy2(item, dst)
shutil.copy2(item, dst)
# 打包为 ZIPzip文件名格式{node_type}-{timestamp}.zip但内部文件夹名称保持为节点类型
# 打包为 ZIPzip文件名格式{node_type}-{timestamp}.zip但内部文件夹名称保持为节点类型 zip_filename = f"{node_type}-{timestamp}.zip"
zip_filename = f"{node_type}-{timestamp}.zip" zip_base_name = tmp_dir / f"{node_type}-{timestamp}"
zip_base_name = tmp_dir / f"{node_type}-{timestamp}" # base_dir 使用 node_type这样 zip 内部文件夹名称就是 node_type
# base_dir 使用 node_type这样 zip 内部文件夹名称就是 node_type shutil.make_archive(str(zip_base_name), 'zip', root_dir=str(tmp_dir), base_dir=node_type)
shutil.make_archive(str(zip_base_name), 'zip', root_dir=str(tmp_dir), base_dir=node_type)
zip_path = tmp_dir / f"{zip_filename}"
zip_path = tmp_dir / f"{zip_filename}"
if not zip_path.exists():
if not zip_path.exists(): raise HTTPException(status_code=500, detail="ZIP文件创建失败")
raise HTTPException(status_code=500, detail="ZIP文件创建失败")
# 读取文件内容
# 读取文件内容 with open(zip_path, 'rb') as f:
with open(zip_path, 'rb') as f: zip_content = f.read()
zip_content = f.read()
# 清理临时文件
# 清理临时文件 if zip_path.exists():
if zip_path.exists(): os.remove(zip_path)
os.remove(zip_path)
# 返回文件内容,前端可以直接使用
# 返回文件内容,前端可以直接使用 return Response(
return Response( content=zip_content,
content=zip_content, media_type="application/zip",
media_type="application/zip", headers={
headers={ "Content-Disposition": f"attachment; filename={zip_filename}",
"Content-Disposition": f"attachment; filename={zip_filename}", "Content-Type": "application/zip"
"Content-Type": "application/zip" }
} )
)
finally:
finally: # 清理临时目录
# 清理临时目录 if temp_package_dir.exists():
if temp_package_dir.exists(): shutil.rmtree(temp_package_dir, ignore_errors=True)
shutil.rmtree(temp_package_dir, ignore_errors=True)
except HTTPException:
except HTTPException: raise
raise except Exception as e:
except Exception as e: logger.error(f"打包节点失败: {str(e)}")
logger.error(f"打包节点失败: {str(e)}") logger.error(f"Traceback: {traceback.format_exc()}")
logger.error(f"Traceback: {traceback.format_exc()}") raise HTTPException(status_code=500, detail=f"打包节点失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"打包节点失败: {str(e)}")
@router.post("/jingrow/node/publish") @router.post("/jingrow/node/publish")