Restore AI workflow design nodes

This commit is contained in:
2026-06-09 21:18:33 +08:00
parent fbd726ebdf
commit cb45c0a068
71 changed files with 22676 additions and 21 deletions
@@ -0,0 +1,302 @@
"""
应用更新节点
执行应用配置的更新操作,将设置保存到数据库
"""
import json
import logging
from typing import Any, Dict, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..base import BaseNode, NodeContext, NodeResult
from ..registry import NodeRegistry
logger = logging.getLogger(__name__)
@NodeRegistry.register
class AppUpdateNode(BaseNode):
"""
应用更新节点
接收应用设置配置,执行实际的更新操作
"""
node_type = 'app_update'
node_name = '应用更新'
node_category = 'application'
node_icon = 'save'
node_description = '执行应用配置的更新操作,将设置保存到数据库'
inputs = [
{
'name': 'settings',
'type': 'object',
'description': '应用设置配置(来自应用设置节点)',
'required': True,
},
{
'name': 'application_id',
'type': 'string',
'description': '应用ID,不传则更新主应用配置',
'required': False,
},
]
outputs = [
{
'name': 'success',
'type': 'boolean',
'description': '更新是否成功',
},
{
'name': 'config_id',
'type': 'string',
'description': '配置记录ID',
},
{
'name': 'message',
'type': 'string',
'description': '更新结果消息',
},
]
async def execute_async(self, context: NodeContext) -> NodeResult:
"""异步执行节点"""
try:
# 获取输入参数
settings = self._get_settings(context)
application_id = self._get_application_id(context)
if not settings:
return NodeResult(
success=False,
error='缺少应用设置配置',
)
logger.info(f'AppUpdateNode - 开始更新应用配置, application_id={application_id}')
# 获取数据库会话
db: AsyncSession = context.db_session
if not db:
return NodeResult(
success=False,
error='数据库会话不可用',
)
# 执行更新
config_id = await self._update_preferences(db, settings, application_id)
if config_id:
logger.info(f'AppUpdateNode - 应用配置更新成功, config_id={config_id}')
return NodeResult(
success=True,
output={
'success': True,
'config_id': config_id,
'message': '应用配置更新成功',
},
output_variables={
'update_success': True,
'config_id': config_id,
'update_message': '应用配置更新成功',
},
)
else:
return NodeResult(
success=False,
error='应用配置更新失败',
)
except Exception as e:
logger.exception(f'应用更新节点执行失败: {e}')
return NodeResult(
success=False,
error=f'节点执行失败: {str(e)}',
)
def execute(self, context: NodeContext) -> NodeResult:
"""同步执行节点(不支持,需要数据库操作)"""
return NodeResult(
success=False,
error='应用更新节点需要异步执行',
)
def _get_settings(self, context: NodeContext) -> Optional[Dict[str, Any]]:
"""获取应用设置配置"""
import re
# 优先从配置中获取(支持变量引用)
settings_config = self.config.get('settings', '')
logger.info(f'AppUpdateNode - settings_config from config: {settings_config}')
if settings_config:
# 检查是否是变量引用格式 {{node_id.key}}
var_pattern = r'\{\{([^}]+)\}\}'
match = re.match(var_pattern, settings_config.strip())
if match:
# 是变量引用,直接从上下文获取对象
var_ref = match.group(1).strip()
logger.info(f'AppUpdateNode - 解析变量引用: {var_ref}')
# 解析 node_id.key 格式
# 注意:node_id 可能包含连字符,如 app_settings-1769009671187
# 所以只按最后一个点分割
if '.' in var_ref:
parts = var_ref.rsplit('.', 1) # 从右边分割,只分割一次
node_id = parts[0]
key = parts[1] if len(parts) > 1 else None
logger.info(f'AppUpdateNode - 查找节点输出: node_id={node_id}, key={key}')
# 从节点输出命名空间获取
node_outputs = context.variables.get(f'_node_{node_id}')
logger.info(f'AppUpdateNode - node_outputs type: {type(node_outputs)}, keys: {node_outputs.keys() if isinstance(node_outputs, dict) else "N/A"}')
if isinstance(node_outputs, dict) and key and key in node_outputs:
value = node_outputs[key]
logger.info(f'AppUpdateNode - 找到值, type: {type(value)}')
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError):
logger.warning(f'AppUpdateNode - JSON解析失败: {value[:100]}')
pass
else:
logger.warning(f'AppUpdateNode - 未找到节点输出或key不存在')
else:
# 直接变量名
value = context.variables.get(var_ref)
logger.info(f'AppUpdateNode - 直接变量引用, type: {type(value)}')
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError):
pass
else:
# 不是变量引用,尝试解析为 JSON
try:
return json.loads(settings_config)
except (json.JSONDecodeError, TypeError):
pass
# 从上下文变量获取
settings = context.variables.get('settings')
logger.info(f'AppUpdateNode - 从上下文获取settings, type: {type(settings)}')
if isinstance(settings, dict):
return settings
if isinstance(settings, str):
try:
return json.loads(settings)
except (json.JSONDecodeError, TypeError):
pass
logger.warning(f'AppUpdateNode - 未能获取有效的settings配置')
return None
def _get_application_id(self, context: NodeContext) -> Optional[str]:
"""获取应用ID"""
# 优先从配置中获取
app_id = self.config.get('application_id', '')
if app_id:
resolved = context.resolve_template(app_id)
if resolved and resolved != 'main':
return resolved
# 从上下文变量获取
app_id = context.variables.get('application_id')
if app_id and app_id != 'main':
return app_id
return None
async def _update_preferences(
self,
db: AsyncSession,
settings: Dict[str, Any],
application_id: Optional[str] = None,
) -> Optional[str]:
"""更新偏好配置"""
from core.ui_config.model import UIConfig
# 配置键:使用与前端 API 一致的键名
config_key = 'frontend_preferences'
# 查找现有配置
stmt = select(UIConfig).where(
UIConfig.config_key == config_key,
UIConfig.is_deleted == False,
)
# 根据 application_id 过滤
if application_id:
stmt = stmt.where(UIConfig.application_id == application_id)
else:
stmt = stmt.where(UIConfig.application_id.is_(None))
result = await db.execute(stmt)
config = result.scalar_one_or_none()
# 配置值
config_value = json.dumps(settings, ensure_ascii=False)
if config:
# 更新现有配置
config.config_value = config_value
await db.commit()
await db.refresh(config)
return config.id
else:
# 创建新配置
new_config = UIConfig(
application_id=application_id,
config_key=config_key,
config_value=config_value,
config_type='preferences',
description='前端UI偏好配置' if not application_id else '子应用UI偏好配置',
status=True,
sort=0,
)
db.add(new_config)
await db.commit()
await db.refresh(new_config)
return new_config.id
@classmethod
def get_config_schema(cls) -> Dict[str, Any]:
"""
获取节点配置 Schema(供前端表单渲染)
"""
return {
'type': 'object',
'properties': {
'settings': {
'type': 'string',
'title': '应用设置',
'description': '应用设置配置,支持变量引用如 {{app_settings-1.settings}}',
'required': True,
'x-component': 'Input',
'x-component-props': {
'placeholder': '请输入应用设置或使用变量引用',
},
},
'application_id': {
'type': 'string',
'title': '应用ID',
'description': '目标应用ID,不填则更新主应用配置。支持变量引用如 {{application_id}}',
'required': False,
'x-component': 'Input',
'x-component-props': {
'placeholder': '留空则更新主应用配置',
},
},
},
'required': ['settings'],
}