259 lines
9.4 KiB
Python
259 lines
9.4 KiB
Python
"""
|
|
应用创建节点
|
|
|
|
创建一个新的应用(子应用)
|
|
"""
|
|
import logging
|
|
from typing import Any, Dict, Optional
|
|
|
|
from ..base import BaseNode, NodeContext, NodeResult
|
|
from ..registry import NodeRegistry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@NodeRegistry.register
|
|
class AppCreateNode(BaseNode):
|
|
"""
|
|
应用创建节点
|
|
|
|
创建一个新的应用(低代码平台的顶层容器)
|
|
"""
|
|
|
|
node_type = 'app_create'
|
|
node_name = '创建应用'
|
|
node_category = 'application'
|
|
node_icon = 'app-window'
|
|
node_description = '创建一个新的应用(子应用)'
|
|
|
|
inputs = [
|
|
{
|
|
'name': 'name',
|
|
'type': 'string',
|
|
'description': '应用名称',
|
|
'required': True,
|
|
},
|
|
{
|
|
'name': 'code',
|
|
'type': 'string',
|
|
'description': '应用编码(唯一标识,用于URL路由,只能包含字母、数字、下划线和连字符,必须以字母开头)',
|
|
'required': True,
|
|
},
|
|
{
|
|
'name': 'description',
|
|
'type': 'string',
|
|
'description': '应用描述',
|
|
'required': False,
|
|
},
|
|
{
|
|
'name': 'icon',
|
|
'type': 'string',
|
|
'description': '应用图标',
|
|
'required': False,
|
|
},
|
|
{
|
|
'name': 'app_type',
|
|
'type': 'string',
|
|
'description': '应用类型: form-表单应用, workflow-流程应用, dashboard-数据应用, screen-大屏应用, mixed-混合应用',
|
|
'required': False,
|
|
},
|
|
]
|
|
|
|
outputs = [
|
|
{
|
|
'name': 'app_id',
|
|
'type': 'string',
|
|
'description': '创建的应用ID',
|
|
},
|
|
{
|
|
'name': 'app_code',
|
|
'type': 'string',
|
|
'description': '应用编码',
|
|
},
|
|
{
|
|
'name': 'app_name',
|
|
'type': 'string',
|
|
'description': '应用名称',
|
|
},
|
|
{
|
|
'name': 'success',
|
|
'type': 'boolean',
|
|
'description': '是否创建成功',
|
|
},
|
|
]
|
|
|
|
def execute(self, context: NodeContext) -> NodeResult:
|
|
"""执行节点(同步方法,通过运行异步方法实现)"""
|
|
import asyncio
|
|
|
|
loop = asyncio.get_event_loop()
|
|
if loop.is_running():
|
|
import concurrent.futures
|
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
future = executor.submit(asyncio.run, self.execute_async(context))
|
|
return future.result()
|
|
else:
|
|
return loop.run_until_complete(self.execute_async(context))
|
|
|
|
async def execute_async(self, context: NodeContext) -> NodeResult:
|
|
"""异步执行节点"""
|
|
try:
|
|
# 获取输入参数并解析变量引用
|
|
name = context.resolve_template(self.config.get('name', ''))
|
|
code_base = context.resolve_template(self.config.get('code', ''))
|
|
description = context.resolve_template(self.config.get('description', ''))
|
|
icon = context.resolve_template(self.config.get('icon', ''))
|
|
app_type = context.resolve_template(self.config.get('app_type', 'mixed'))
|
|
|
|
# 配置选项
|
|
update_if_exists = self.config.get('update_if_exists', False)
|
|
|
|
logger.info(f'AppCreateNode - 开始创建应用: name={name}, code={code_base}')
|
|
|
|
# 验证必要参数
|
|
if not name:
|
|
return NodeResult(
|
|
success=False,
|
|
error='应用名称不能为空',
|
|
)
|
|
|
|
if not code_base:
|
|
return NodeResult(
|
|
success=False,
|
|
error='应用编码不能为空',
|
|
)
|
|
|
|
# 验证编码格式(字母开头,只能包含字母、数字、下划线和连字符)
|
|
import re
|
|
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', code_base):
|
|
return NodeResult(
|
|
success=False,
|
|
error='应用编码格式不正确,必须以字母开头,只能包含字母、数字、下划线和连字符',
|
|
)
|
|
|
|
# 导入服务和数据库会话
|
|
from app.database import AsyncSessionLocal
|
|
from core.application.service import ApplicationService
|
|
from core.application.schema import ApplicationCreate, ApplicationUpdate
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
# 生成唯一的 code(如果已存在则自动添加后缀)
|
|
code = await self._get_unique_code(db, code_base, ApplicationService)
|
|
logger.info(f'AppCreateNode - 使用 code: {code} (原始: {code_base})')
|
|
|
|
# 检查应用编码是否已存在(理论上不应该存在,因为已经生成了唯一 code)
|
|
existing_app = await ApplicationService.get_by_code(db, code)
|
|
|
|
if existing_app:
|
|
if update_if_exists:
|
|
# 更新已存在的应用
|
|
update_data = ApplicationUpdate(
|
|
name=name,
|
|
description=description or None,
|
|
icon=icon or None,
|
|
app_type=app_type or None,
|
|
)
|
|
updated_app = await ApplicationService.update(
|
|
db,
|
|
record_id=existing_app.id,
|
|
data=update_data,
|
|
auto_commit=True
|
|
)
|
|
|
|
if updated_app:
|
|
logger.info(f'AppCreateNode - 应用已存在,更新成功: id={updated_app.id}')
|
|
return NodeResult(
|
|
success=True,
|
|
output=f'应用 {name} 更新成功',
|
|
output_variables={
|
|
'app_id': updated_app.id,
|
|
'app_code': updated_app.code,
|
|
'app_name': updated_app.name,
|
|
'success': True,
|
|
}
|
|
)
|
|
else:
|
|
return NodeResult(
|
|
success=False,
|
|
error=f'更新应用失败',
|
|
)
|
|
else:
|
|
# 应用已存在,返回已存在的应用信息
|
|
logger.info(f'AppCreateNode - 应用编码已存在: code={code}, id={existing_app.id}')
|
|
return NodeResult(
|
|
success=True,
|
|
output=f'应用编码 {code} 已存在',
|
|
output_variables={
|
|
'app_id': existing_app.id,
|
|
'app_code': existing_app.code,
|
|
'app_name': existing_app.name,
|
|
'success': False, # 标记为未创建新应用
|
|
}
|
|
)
|
|
|
|
# 创建新应用
|
|
create_data = ApplicationCreate(
|
|
name=name,
|
|
code=code,
|
|
description=description or '',
|
|
icon=icon or '',
|
|
app_type=app_type or 'mixed',
|
|
)
|
|
|
|
new_app = await ApplicationService.create(db, data=create_data, auto_commit=True)
|
|
|
|
if new_app:
|
|
logger.info(f'AppCreateNode - 应用创建成功: id={new_app.id}, code={new_app.code}')
|
|
return NodeResult(
|
|
success=True,
|
|
output=f'应用 {name} 创建成功',
|
|
output_variables={
|
|
'app_id': new_app.id,
|
|
'app_code': new_app.code,
|
|
'app_name': new_app.name,
|
|
'success': True,
|
|
}
|
|
)
|
|
else:
|
|
return NodeResult(
|
|
success=False,
|
|
error='创建应用失败',
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.exception(f'AppCreateNode - 执行异常: {e}')
|
|
return NodeResult(
|
|
success=False,
|
|
error=f'创建应用异常: {str(e)}',
|
|
)
|
|
|
|
async def _get_unique_code(self, db, base_code: str, service) -> str:
|
|
"""
|
|
生成唯一的 code,如果已存在则自动添加数字后缀
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
base_code: 基础 code
|
|
service: 服务类(需要有 get_by_code 方法)
|
|
|
|
Returns:
|
|
唯一的 code
|
|
"""
|
|
code = base_code
|
|
counter = 1
|
|
|
|
while True:
|
|
try:
|
|
existing = await service.get_by_code(db, code)
|
|
if not existing:
|
|
# code 不存在,可以使用
|
|
break
|
|
# code 已存在,添加后缀
|
|
code = f"{base_code}_{counter}"
|
|
counter += 1
|
|
except Exception:
|
|
# 查询出错,认为 code 可用
|
|
break
|
|
|
|
return code
|