工作流动画效果
This commit is contained in:
@@ -331,3 +331,69 @@ async def stop_agent(
|
|||||||
|
|
||||||
logger.info(f"用户 {current_user.username} 停止了Agent: {agent.name} ({agent_id})")
|
logger.info(f"用户 {current_user.username} 停止了Agent: {agent.name} ({agent_id})")
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
|
|
||||||
|
class AgentDuplicateRequest(BaseModel):
|
||||||
|
"""Agent复制请求模型"""
|
||||||
|
name: Optional[str] = None # 如果提供,使用此名称;否则自动生成
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{agent_id}/duplicate", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def duplicate_agent(
|
||||||
|
agent_id: str,
|
||||||
|
duplicate_data: Optional[AgentDuplicateRequest] = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
复制Agent
|
||||||
|
|
||||||
|
创建一个新的Agent副本,包含原Agent的所有配置(工作流、描述等)
|
||||||
|
新Agent的状态为draft,版本号为1
|
||||||
|
"""
|
||||||
|
# 获取原Agent
|
||||||
|
original_agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||||
|
|
||||||
|
if not original_agent:
|
||||||
|
raise NotFoundError(f"Agent不存在: {agent_id}")
|
||||||
|
|
||||||
|
# 检查权限:read权限(需要能读取原Agent才能复制)
|
||||||
|
if not check_agent_permission(db, current_user, original_agent, "read"):
|
||||||
|
raise HTTPException(status_code=403, detail="无权复制此Agent")
|
||||||
|
|
||||||
|
# 生成新名称
|
||||||
|
if duplicate_data and duplicate_data.name:
|
||||||
|
new_name = duplicate_data.name
|
||||||
|
else:
|
||||||
|
# 自动生成名称:原名称 + " (副本)"
|
||||||
|
base_name = original_agent.name
|
||||||
|
new_name = f"{base_name} (副本)"
|
||||||
|
|
||||||
|
# 如果名称已存在,添加序号
|
||||||
|
counter = 1
|
||||||
|
while db.query(Agent).filter(
|
||||||
|
Agent.name == new_name,
|
||||||
|
Agent.user_id == current_user.id
|
||||||
|
).first():
|
||||||
|
counter += 1
|
||||||
|
new_name = f"{base_name} (副本 {counter})"
|
||||||
|
|
||||||
|
# 深拷贝工作流配置(避免引用问题)
|
||||||
|
import copy
|
||||||
|
new_workflow_config = copy.deepcopy(original_agent.workflow_config)
|
||||||
|
|
||||||
|
# 创建新Agent
|
||||||
|
new_agent = Agent(
|
||||||
|
name=new_name,
|
||||||
|
description=original_agent.description,
|
||||||
|
workflow_config=new_workflow_config,
|
||||||
|
user_id=current_user.id,
|
||||||
|
status="draft", # 复制的Agent状态为草稿
|
||||||
|
version=1 # 版本号从1开始
|
||||||
|
)
|
||||||
|
db.add(new_agent)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_agent)
|
||||||
|
|
||||||
|
logger.info(f"用户 {current_user.username} 复制了Agent: {original_agent.name} ({agent_id}) -> {new_agent.name} ({new_agent.id})")
|
||||||
|
return new_agent
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from app.services.llm_service import llm_service
|
from app.services.llm_service import llm_service
|
||||||
from app.services.condition_parser import condition_parser
|
from app.services.condition_parser import condition_parser
|
||||||
from app.services.data_transformer import data_transformer
|
from app.services.data_transformer import data_transformer
|
||||||
@@ -123,10 +124,89 @@ class WorkflowEngine:
|
|||||||
else:
|
else:
|
||||||
# 否则合并所有输入
|
# 否则合并所有输入
|
||||||
if isinstance(source_output, dict):
|
if isinstance(source_output, dict):
|
||||||
input_data.update(source_output)
|
# 如果source_output包含output字段,展开它
|
||||||
|
if 'output' in source_output and isinstance(source_output['output'], dict):
|
||||||
|
# 将output中的内容展开到顶层
|
||||||
|
input_data.update(source_output['output'])
|
||||||
|
# 保留其他字段(如status)
|
||||||
|
for key, value in source_output.items():
|
||||||
|
if key != 'output':
|
||||||
|
input_data[key] = value
|
||||||
|
else:
|
||||||
|
input_data.update(source_output)
|
||||||
else:
|
else:
|
||||||
input_data['input'] = source_output
|
input_data['input'] = source_output
|
||||||
|
|
||||||
|
# 如果input_data中没有query字段,尝试从所有已执行的节点中查找(特别是start节点)
|
||||||
|
if 'query' not in input_data:
|
||||||
|
# 优先查找start节点
|
||||||
|
for node_id_key in ['start-1', 'start']:
|
||||||
|
if node_id_key in node_outputs:
|
||||||
|
node_output = node_outputs[node_id_key]
|
||||||
|
if isinstance(node_output, dict):
|
||||||
|
# 检查顶层字段(因为node_outputs存储的是output字段的内容)
|
||||||
|
if 'query' in node_output:
|
||||||
|
input_data['query'] = node_output['query']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取query: {input_data['query']}")
|
||||||
|
break
|
||||||
|
# 检查output字段(兼容性)
|
||||||
|
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||||
|
if 'query' in node_output['output']:
|
||||||
|
input_data['query'] = node_output['output']['query']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取query: {input_data['query']}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果还没找到,遍历所有节点
|
||||||
|
if 'query' not in input_data:
|
||||||
|
for node_id_key, node_output in node_outputs.items():
|
||||||
|
if isinstance(node_output, dict):
|
||||||
|
# 检查顶层字段
|
||||||
|
if 'query' in node_output:
|
||||||
|
input_data['query'] = node_output['query']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取query: {input_data['query']}")
|
||||||
|
break
|
||||||
|
# 检查output字段(兼容性)
|
||||||
|
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||||
|
if 'query' in node_output['output']:
|
||||||
|
input_data['query'] = node_output['output']['query']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取query: {input_data['query']}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果input_data中没有requirement_analysis字段,尝试从所有已执行的节点中查找
|
||||||
|
if 'requirement_analysis' not in input_data:
|
||||||
|
# 优先查找requirement-analysis节点
|
||||||
|
for node_id_key in ['llm-requirement-analysis', 'requirement-analysis']:
|
||||||
|
if node_id_key in node_outputs:
|
||||||
|
node_output = node_outputs[node_id_key]
|
||||||
|
if isinstance(node_output, dict):
|
||||||
|
# 检查顶层字段(因为node_outputs存储的是output字段的内容)
|
||||||
|
if 'requirement_analysis' in node_output:
|
||||||
|
input_data['requirement_analysis'] = node_output['requirement_analysis']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取requirement_analysis")
|
||||||
|
break
|
||||||
|
# 检查output字段(兼容性)
|
||||||
|
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||||
|
if 'requirement_analysis' in node_output['output']:
|
||||||
|
input_data['requirement_analysis'] = node_output['output']['requirement_analysis']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取requirement_analysis")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果还没找到,遍历所有节点
|
||||||
|
if 'requirement_analysis' not in input_data:
|
||||||
|
for node_id_key, node_output in node_outputs.items():
|
||||||
|
if isinstance(node_output, dict):
|
||||||
|
# 检查顶层字段
|
||||||
|
if 'requirement_analysis' in node_output:
|
||||||
|
input_data['requirement_analysis'] = node_output['requirement_analysis']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 中获取requirement_analysis")
|
||||||
|
break
|
||||||
|
# 检查output字段(兼容性)
|
||||||
|
elif 'output' in node_output and isinstance(node_output['output'], dict):
|
||||||
|
if 'requirement_analysis' in node_output['output']:
|
||||||
|
input_data['requirement_analysis'] = node_output['output']['requirement_analysis']
|
||||||
|
logger.debug(f"[rjb] 从节点 {node_id_key} 的output中获取requirement_analysis")
|
||||||
|
break
|
||||||
|
|
||||||
logger.debug(f"[rjb] 节点输入结果: node_id={node_id}, input_data={input_data}")
|
logger.debug(f"[rjb] 节点输入结果: node_id={node_id}, input_data={input_data}")
|
||||||
return input_data
|
return input_data
|
||||||
|
|
||||||
@@ -344,27 +424,50 @@ class WorkflowEngine:
|
|||||||
try:
|
try:
|
||||||
# 将input_data转换为字符串用于格式化
|
# 将input_data转换为字符串用于格式化
|
||||||
if isinstance(input_data, dict):
|
if isinstance(input_data, dict):
|
||||||
# 如果prompt中包含变量,尝试格式化
|
# 支持两种格式的变量:{key} 和 {{key}}
|
||||||
if '{' in prompt and '}' in prompt:
|
formatted_prompt = prompt
|
||||||
# 尝试格式化所有input_data中的键
|
has_unfilled_variables = False
|
||||||
formatted_prompt = prompt
|
|
||||||
for key, value in input_data.items():
|
# 首先处理 {{variable}} 格式(模板节点常用)
|
||||||
placeholder = f'{{{key}}}'
|
import re
|
||||||
if placeholder in formatted_prompt:
|
double_brace_vars = re.findall(r'\{\{(\w+)\}\}', prompt)
|
||||||
formatted_prompt = formatted_prompt.replace(
|
for var_name in double_brace_vars:
|
||||||
placeholder,
|
if var_name in input_data:
|
||||||
json_module.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
# 替换 {{variable}} 为实际值
|
||||||
)
|
value = input_data[var_name]
|
||||||
# 如果还有{input}占位符,替换为整个input_data
|
replacement = json_module.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
||||||
if '{input}' in formatted_prompt:
|
formatted_prompt = formatted_prompt.replace(f'{{{{{var_name}}}}}', replacement)
|
||||||
|
else:
|
||||||
|
has_unfilled_variables = True
|
||||||
|
|
||||||
|
# 然后处理 {key} 格式
|
||||||
|
for key, value in input_data.items():
|
||||||
|
placeholder = f'{{{key}}}'
|
||||||
|
if placeholder in formatted_prompt:
|
||||||
formatted_prompt = formatted_prompt.replace(
|
formatted_prompt = formatted_prompt.replace(
|
||||||
'{input}',
|
placeholder,
|
||||||
json_module.dumps(input_data, ensure_ascii=False)
|
json_module.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else str(value)
|
||||||
)
|
)
|
||||||
prompt = formatted_prompt
|
|
||||||
else:
|
# 如果还有{input}占位符,替换为整个input_data
|
||||||
# 如果没有占位符,将input_data作为JSON附加到prompt
|
if '{input}' in formatted_prompt:
|
||||||
prompt = f"{prompt}\n\n输入数据:\n{json_module.dumps(input_data, ensure_ascii=False)}"
|
formatted_prompt = formatted_prompt.replace(
|
||||||
|
'{input}',
|
||||||
|
json_module.dumps(input_data, ensure_ascii=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果仍有未填充的变量({{variable}}格式),将用户输入作为上下文附加
|
||||||
|
if has_unfilled_variables or re.search(r'\{\{(\w+)\}\}', formatted_prompt):
|
||||||
|
# 提取用户的实际查询内容
|
||||||
|
user_query = input_data.get('query', input_data.get('input', input_data.get('text', '')))
|
||||||
|
if not user_query and isinstance(input_data, dict):
|
||||||
|
# 如果没有明确的query字段,尝试从整个input_data中提取文本内容
|
||||||
|
user_query = json_module.dumps(input_data, ensure_ascii=False)
|
||||||
|
|
||||||
|
if user_query:
|
||||||
|
formatted_prompt = f"{formatted_prompt}\n\n用户需求:{user_query}\n\n请根据以上用户需求,忽略未填充的变量占位符(如{{{{variable}}}}),直接基于用户需求来完成任务。"
|
||||||
|
|
||||||
|
prompt = formatted_prompt
|
||||||
else:
|
else:
|
||||||
# 如果input_data不是dict,直接转换为字符串
|
# 如果input_data不是dict,直接转换为字符串
|
||||||
if '{input}' in prompt:
|
if '{input}' in prompt:
|
||||||
@@ -373,6 +476,7 @@ class WorkflowEngine:
|
|||||||
prompt = f"{prompt}\n\n输入:{str(input_data)}"
|
prompt = f"{prompt}\n\n输入:{str(input_data)}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 格式化失败,使用原始prompt和input_data
|
# 格式化失败,使用原始prompt和input_data
|
||||||
|
logger.warning(f"[rjb] Prompt格式化失败: {str(e)}")
|
||||||
try:
|
try:
|
||||||
prompt = f"{prompt}\n\n输入数据:\n{json_module.dumps(input_data, ensure_ascii=False)}"
|
prompt = f"{prompt}\n\n输入数据:\n{json_module.dumps(input_data, ensure_ascii=False)}"
|
||||||
except:
|
except:
|
||||||
@@ -381,8 +485,27 @@ class WorkflowEngine:
|
|||||||
# 获取LLM配置
|
# 获取LLM配置
|
||||||
provider = node_data.get('provider', 'openai')
|
provider = node_data.get('provider', 'openai')
|
||||||
model = node_data.get('model', 'gpt-3.5-turbo')
|
model = node_data.get('model', 'gpt-3.5-turbo')
|
||||||
temperature = node_data.get('temperature', 0.7)
|
# 确保temperature是浮点数(节点模板中可能是字符串)
|
||||||
max_tokens = node_data.get('max_tokens')
|
temperature_raw = node_data.get('temperature', 0.7)
|
||||||
|
if isinstance(temperature_raw, str):
|
||||||
|
try:
|
||||||
|
temperature = float(temperature_raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
temperature = 0.7
|
||||||
|
else:
|
||||||
|
temperature = float(temperature_raw) if temperature_raw is not None else 0.7
|
||||||
|
# 确保max_tokens是整数(节点模板中可能是字符串)
|
||||||
|
max_tokens_raw = node_data.get('max_tokens')
|
||||||
|
if max_tokens_raw is not None:
|
||||||
|
if isinstance(max_tokens_raw, str):
|
||||||
|
try:
|
||||||
|
max_tokens = int(max_tokens_raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
max_tokens = None
|
||||||
|
else:
|
||||||
|
max_tokens = int(max_tokens_raw) if max_tokens_raw is not None else None
|
||||||
|
else:
|
||||||
|
max_tokens = None
|
||||||
# 不传递 api_key 和 base_url,让 LLM 服务使用系统默认配置(与节点测试保持一致)
|
# 不传递 api_key 和 base_url,让 LLM 服务使用系统默认配置(与节点测试保持一致)
|
||||||
api_key = None
|
api_key = None
|
||||||
base_url = None
|
base_url = None
|
||||||
@@ -461,13 +584,68 @@ class WorkflowEngine:
|
|||||||
mode = node_data.get('mode', 'mapping')
|
mode = node_data.get('mode', 'mapping')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = data_transformer.transform_data(
|
# 处理mapping中的{{variable}}格式,从input_data中提取值
|
||||||
input_data=input_data,
|
# 首先,如果input_data包含output字段,需要展开它
|
||||||
mapping=mapping,
|
expanded_input = input_data.copy()
|
||||||
filter_rules=filter_rules,
|
if 'output' in input_data and isinstance(input_data['output'], dict):
|
||||||
compute_rules=compute_rules,
|
# 将output中的内容展开到顶层,但保留output字段
|
||||||
mode=mode
|
expanded_input.update(input_data['output'])
|
||||||
)
|
|
||||||
|
processed_mapping = {}
|
||||||
|
import re
|
||||||
|
for target_key, source_expr in mapping.items():
|
||||||
|
if isinstance(source_expr, str):
|
||||||
|
# 支持{{variable}}格式
|
||||||
|
double_brace_vars = re.findall(r'\{\{(\w+)\}\}', source_expr)
|
||||||
|
if double_brace_vars:
|
||||||
|
# 从expanded_input中获取变量值
|
||||||
|
var_value = None
|
||||||
|
for var_name in double_brace_vars:
|
||||||
|
# 尝试从expanded_input中获取,支持嵌套路径
|
||||||
|
var_value = self._get_nested_value(expanded_input, var_name)
|
||||||
|
if var_value is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if var_value is not None:
|
||||||
|
# 如果只有一个变量,直接使用值;否则替换表达式
|
||||||
|
if len(double_brace_vars) == 1:
|
||||||
|
processed_mapping[target_key] = var_value
|
||||||
|
else:
|
||||||
|
# 多个变量,替换表达式
|
||||||
|
processed_expr = source_expr
|
||||||
|
for var_name in double_brace_vars:
|
||||||
|
var_val = self._get_nested_value(expanded_input, var_name)
|
||||||
|
if var_val is not None:
|
||||||
|
replacement = json_module.dumps(var_val, ensure_ascii=False) if isinstance(var_val, (dict, list)) else str(var_val)
|
||||||
|
processed_expr = processed_expr.replace(f'{{{{{var_name}}}}}', replacement)
|
||||||
|
processed_mapping[target_key] = processed_expr
|
||||||
|
else:
|
||||||
|
# 变量不存在,保持原表达式
|
||||||
|
processed_mapping[target_key] = source_expr
|
||||||
|
else:
|
||||||
|
# 不是{{variable}}格式,直接使用
|
||||||
|
processed_mapping[target_key] = source_expr
|
||||||
|
else:
|
||||||
|
# 不是字符串,直接使用
|
||||||
|
processed_mapping[target_key] = source_expr
|
||||||
|
|
||||||
|
# 如果mode是merge,需要合并所有输入数据
|
||||||
|
if mode == 'merge':
|
||||||
|
# 合并所有上游节点的输出(使用展开后的数据)
|
||||||
|
result = expanded_input.copy()
|
||||||
|
# 添加mapping的结果
|
||||||
|
for key, value in processed_mapping.items():
|
||||||
|
result[key] = value
|
||||||
|
else:
|
||||||
|
# 使用处理后的mapping进行转换(使用展开后的数据)
|
||||||
|
result = data_transformer.transform_data(
|
||||||
|
input_data=expanded_input,
|
||||||
|
mapping=processed_mapping,
|
||||||
|
filter_rules=filter_rules,
|
||||||
|
compute_rules=compute_rules,
|
||||||
|
mode=mode
|
||||||
|
)
|
||||||
|
|
||||||
exec_result = {'output': result, 'status': 'success'}
|
exec_result = {'output': result, 'status': 'success'}
|
||||||
if self.logger:
|
if self.logger:
|
||||||
duration = int((time.time() - start_time) * 1000)
|
duration = int((time.time() - start_time) * 1000)
|
||||||
@@ -477,6 +655,7 @@ class WorkflowEngine:
|
|||||||
if self.logger:
|
if self.logger:
|
||||||
duration = int((time.time() - start_time) * 1000)
|
duration = int((time.time() - start_time) * 1000)
|
||||||
self.logger.log_node_error(node_id, node_type, e, duration)
|
self.logger.log_node_error(node_id, node_type, e, duration)
|
||||||
|
logger.error(f"[rjb] Transform节点执行失败: {str(e)}", exc_info=True)
|
||||||
return {
|
return {
|
||||||
'output': None,
|
'output': None,
|
||||||
'status': 'failed',
|
'status': 'failed',
|
||||||
@@ -1430,45 +1609,120 @@ class WorkflowEngine:
|
|||||||
|
|
||||||
elif node_type == 'output' or node_type == 'end':
|
elif node_type == 'output' or node_type == 'end':
|
||||||
# 输出节点:返回最终结果
|
# 输出节点:返回最终结果
|
||||||
# 对于人机交互场景,End节点应该返回纯文本字符串,而不是JSON
|
# 读取节点配置中的输出格式设置
|
||||||
logger.debug(f"[rjb] End节点处理: node_id={node_id}, input_data={input_data}, input_data type={type(input_data)}")
|
node_data = node.get('data', {})
|
||||||
|
output_format = node_data.get('output_format', 'text') # 默认纯文本
|
||||||
|
|
||||||
|
logger.debug(f"[rjb] End节点处理: node_id={node_id}, output_format={output_format}, input_data={input_data}, input_data type={type(input_data)}")
|
||||||
final_output = input_data
|
final_output = input_data
|
||||||
|
|
||||||
# 递归解包,提取实际的文本内容
|
# 如果配置为JSON格式,直接返回原始数据(或格式化的JSON)
|
||||||
if isinstance(input_data, dict):
|
if output_format == 'json':
|
||||||
# 如果只有一个 key 且是 'input',提取其值
|
# 如果是字典,直接返回JSON格式
|
||||||
if len(input_data) == 1 and 'input' in input_data:
|
if isinstance(input_data, dict):
|
||||||
final_output = input_data['input']
|
final_output = json_module.dumps(input_data, ensure_ascii=False, indent=2)
|
||||||
logger.debug(f"[rjb] End节点提取第一层: final_output={final_output}, type={type(final_output)}")
|
elif isinstance(input_data, str):
|
||||||
# 如果提取的值仍然是字典且只有一个 'input' key,继续提取
|
# 尝试解析为JSON,如果成功则格式化,否则直接返回
|
||||||
if isinstance(final_output, dict) and len(final_output) == 1 and 'input' in final_output:
|
try:
|
||||||
final_output = final_output['input']
|
parsed = json_module.loads(input_data)
|
||||||
logger.debug(f"[rjb] End节点提取第二层: final_output={final_output}, type={type(final_output)}")
|
final_output = json_module.dumps(parsed, ensure_ascii=False, indent=2)
|
||||||
|
except:
|
||||||
# 确保最终输出是字符串(对于人机交互场景)
|
final_output = input_data
|
||||||
# 如果是字典,尝试转换为字符串;如果是其他类型,也转换为字符串
|
|
||||||
if not isinstance(final_output, str):
|
|
||||||
if isinstance(final_output, dict):
|
|
||||||
# 如果是字典,尝试提取文本内容或转换为JSON字符串
|
|
||||||
# 优先查找常见的文本字段
|
|
||||||
if 'text' in final_output:
|
|
||||||
final_output = str(final_output['text'])
|
|
||||||
elif 'content' in final_output:
|
|
||||||
final_output = str(final_output['content'])
|
|
||||||
elif 'message' in final_output:
|
|
||||||
final_output = str(final_output['message'])
|
|
||||||
elif 'response' in final_output:
|
|
||||||
final_output = str(final_output['response'])
|
|
||||||
elif len(final_output) == 1:
|
|
||||||
# 如果只有一个key,直接使用其值
|
|
||||||
final_output = str(list(final_output.values())[0])
|
|
||||||
else:
|
|
||||||
# 否则转换为JSON字符串
|
|
||||||
final_output = json_module.dumps(final_output, ensure_ascii=False)
|
|
||||||
else:
|
else:
|
||||||
final_output = str(final_output)
|
final_output = json_module.dumps({'output': input_data}, ensure_ascii=False, indent=2)
|
||||||
|
else:
|
||||||
|
# 默认纯文本格式:递归解包,提取实际的文本内容
|
||||||
|
if isinstance(input_data, dict):
|
||||||
|
# 优先提取 'output' 字段(LLM节点的标准输出格式)
|
||||||
|
if 'output' in input_data and isinstance(input_data['output'], str):
|
||||||
|
final_output = input_data['output']
|
||||||
|
# 如果只有一个 key 且是 'input',提取其值
|
||||||
|
elif len(input_data) == 1 and 'input' in input_data:
|
||||||
|
final_output = input_data['input']
|
||||||
|
# 如果包含 'solution' 字段,提取其值
|
||||||
|
elif 'solution' in input_data and isinstance(input_data['solution'], str):
|
||||||
|
final_output = input_data['solution']
|
||||||
|
# 如果input_data是字符串类型的字典(JSON字符串),尝试解析
|
||||||
|
elif isinstance(input_data, str):
|
||||||
|
try:
|
||||||
|
parsed = json_module.loads(input_data)
|
||||||
|
if isinstance(parsed, dict) and 'output' in parsed:
|
||||||
|
final_output = parsed['output']
|
||||||
|
elif isinstance(parsed, str):
|
||||||
|
final_output = parsed
|
||||||
|
except:
|
||||||
|
final_output = input_data
|
||||||
|
logger.debug(f"[rjb] End节点提取第一层: final_output={final_output}, type={type(final_output)}")
|
||||||
|
# 如果提取的值仍然是字典且只有一个 'input' key,继续提取
|
||||||
|
if isinstance(final_output, dict) and len(final_output) == 1 and 'input' in final_output:
|
||||||
|
final_output = final_output['input']
|
||||||
|
logger.debug(f"[rjb] End节点提取第二层: final_output={final_output}, type={type(final_output)}")
|
||||||
|
|
||||||
logger.debug(f"[rjb] End节点最终输出: final_output={final_output}, type={type(final_output)}")
|
# 确保最终输出是字符串(对于人机交互场景)
|
||||||
|
# 如果是字典,尝试转换为字符串;如果是其他类型,也转换为字符串
|
||||||
|
if not isinstance(final_output, str):
|
||||||
|
if isinstance(final_output, dict):
|
||||||
|
# 如果是字典,尝试提取文本内容或转换为JSON字符串
|
||||||
|
# 优先查找常见的文本字段
|
||||||
|
if 'text' in final_output:
|
||||||
|
final_output = str(final_output['text'])
|
||||||
|
elif 'content' in final_output:
|
||||||
|
final_output = str(final_output['content'])
|
||||||
|
elif 'message' in final_output:
|
||||||
|
final_output = str(final_output['message'])
|
||||||
|
elif 'response' in final_output:
|
||||||
|
final_output = str(final_output['response'])
|
||||||
|
elif len(final_output) == 1:
|
||||||
|
# 如果只有一个key,直接使用其值
|
||||||
|
final_output = str(list(final_output.values())[0])
|
||||||
|
else:
|
||||||
|
# 否则转换为纯文本(不是JSON)
|
||||||
|
# 尝试提取所有文本字段并组合,但排除系统字段
|
||||||
|
text_parts = []
|
||||||
|
exclude_keys = {'status', 'error', 'timestamp', 'node_id', 'execution_time'}
|
||||||
|
for key, value in final_output.items():
|
||||||
|
if key in exclude_keys:
|
||||||
|
continue
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
# 如果值本身已经包含 "key: " 格式,直接使用
|
||||||
|
if value.strip().startswith(f"{key}:"):
|
||||||
|
text_parts.append(value.strip())
|
||||||
|
else:
|
||||||
|
text_parts.append(value.strip())
|
||||||
|
elif isinstance(value, (int, float, bool)):
|
||||||
|
text_parts.append(f"{key}: {value}")
|
||||||
|
if text_parts:
|
||||||
|
final_output = "\n".join(text_parts)
|
||||||
|
else:
|
||||||
|
final_output = str(final_output)
|
||||||
|
else:
|
||||||
|
final_output = str(final_output)
|
||||||
|
|
||||||
|
# 清理输出文本:移除常见的字段前缀(如 "input: ", "query: " 等)
|
||||||
|
if isinstance(final_output, str):
|
||||||
|
import re
|
||||||
|
# 移除行首的 "input: ", "query: ", "output: " 等前缀
|
||||||
|
lines = final_output.split('\n')
|
||||||
|
cleaned_lines = []
|
||||||
|
for line in lines:
|
||||||
|
# 匹配行首的 "字段名: " 格式并移除
|
||||||
|
# 但保留内容本身
|
||||||
|
line = re.sub(r'^(input|query|output|result|response|message|content|text):\s*', '', line, flags=re.IGNORECASE)
|
||||||
|
if line.strip(): # 只保留非空行
|
||||||
|
cleaned_lines.append(line)
|
||||||
|
|
||||||
|
# 如果清理后还有内容,使用清理后的版本
|
||||||
|
if cleaned_lines:
|
||||||
|
final_output = '\n'.join(cleaned_lines)
|
||||||
|
# 如果清理后为空,使用原始输出(避免丢失所有内容)
|
||||||
|
elif final_output.strip():
|
||||||
|
# 如果原始输出不为空,但清理后为空,说明可能格式特殊,尝试更宽松的清理
|
||||||
|
# 只移除明显的 "input: " 和 "query: " 前缀,保留其他内容
|
||||||
|
final_output = re.sub(r'^(input|query):\s*', '', final_output, flags=re.IGNORECASE | re.MULTILINE)
|
||||||
|
if not final_output.strip():
|
||||||
|
final_output = str(input_data) # 如果还是空,使用原始输入
|
||||||
|
|
||||||
|
logger.debug(f"[rjb] End节点最终输出: output_format={output_format}, final_output={final_output[:100] if isinstance(final_output, str) else type(final_output)}")
|
||||||
result = {'output': final_output, 'status': 'success'}
|
result = {'output': final_output, 'status': 'success'}
|
||||||
if self.logger:
|
if self.logger:
|
||||||
duration = int((time.time() - start_time) * 1000)
|
duration = int((time.time() - start_time) * 1000)
|
||||||
|
|||||||
309
backend/scripts/generate_complex_agent.py
Executable file
309
backend/scripts/generate_complex_agent.py
Executable file
@@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
生成包含多个节点模板的复杂Agent
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models.agent import Agent
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.node_template import NodeTemplate
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def generate_complex_agent_with_templates(db: Session, username: str = "admin"):
|
||||||
|
"""生成包含多个节点模板的复杂Agent"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("生成复杂Agent(包含多个节点模板)")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 查找用户
|
||||||
|
user = db.query(User).filter(User.username == username).first()
|
||||||
|
if not user:
|
||||||
|
print(f"❌ 未找到用户 '{username}',请先创建该用户")
|
||||||
|
return
|
||||||
|
print(f"✅ 找到用户: {user.username} (ID: {user.id})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 查找可用的节点模板(公开的或用户自己的)
|
||||||
|
templates = db.query(NodeTemplate).filter(
|
||||||
|
(NodeTemplate.is_public == True) | (NodeTemplate.user_id == user.id)
|
||||||
|
).limit(10).all()
|
||||||
|
|
||||||
|
if len(templates) < 3:
|
||||||
|
print(f"⚠️ 警告: 只找到 {len(templates)} 个节点模板,建议至少3个")
|
||||||
|
if len(templates) == 0:
|
||||||
|
print("❌ 没有可用的节点模板,请先创建节点模板")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📋 找到 {len(templates)} 个可用节点模板:")
|
||||||
|
for i, template in enumerate(templates[:5], 1):
|
||||||
|
print(f" {i}. {template.name} (ID: {template.id})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 选择3-5个模板用于创建复杂Agent
|
||||||
|
selected_templates = templates[:min(5, len(templates))]
|
||||||
|
template_ids = [t.id for t in selected_templates]
|
||||||
|
|
||||||
|
# 生成复杂的工作流配置
|
||||||
|
# 工作流结构:开始 -> 模板1 -> 条件判断 -> 模板2/模板3 -> 转换 -> 模板4 -> 结束
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
# 开始节点
|
||||||
|
start_node = {
|
||||||
|
"id": "start-1",
|
||||||
|
"type": "start",
|
||||||
|
"position": {"x": 100, "y": 200},
|
||||||
|
"data": {"label": "开始"}
|
||||||
|
}
|
||||||
|
nodes.append(start_node)
|
||||||
|
|
||||||
|
# 第一个模板节点
|
||||||
|
if len(template_ids) > 0:
|
||||||
|
template1_node = {
|
||||||
|
"id": "template-1",
|
||||||
|
"type": "template",
|
||||||
|
"position": {"x": 300, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": selected_templates[0].name,
|
||||||
|
"template_id": template_ids[0],
|
||||||
|
"provider": selected_templates[0].provider,
|
||||||
|
"model": selected_templates[0].model,
|
||||||
|
"temperature": selected_templates[0].temperature,
|
||||||
|
"max_tokens": selected_templates[0].max_tokens,
|
||||||
|
"prompt": selected_templates[0].prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(template1_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e1",
|
||||||
|
"source": "start-1",
|
||||||
|
"target": "template-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 条件判断节点
|
||||||
|
condition_node = {
|
||||||
|
"id": "condition-1",
|
||||||
|
"type": "condition",
|
||||||
|
"position": {"x": 500, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": "判断处理结果",
|
||||||
|
"condition": "{{result}} contains '需要' or {{result}} contains '重要'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(condition_node)
|
||||||
|
if len(template_ids) > 0:
|
||||||
|
edges.append({
|
||||||
|
"id": "e2",
|
||||||
|
"source": "template-1",
|
||||||
|
"target": "condition-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 条件分支:如果满足条件,使用模板2;否则使用模板3
|
||||||
|
if len(template_ids) > 1:
|
||||||
|
template2_node = {
|
||||||
|
"id": "template-2",
|
||||||
|
"type": "template",
|
||||||
|
"position": {"x": 700, "y": 100},
|
||||||
|
"data": {
|
||||||
|
"label": selected_templates[1].name,
|
||||||
|
"template_id": template_ids[1],
|
||||||
|
"provider": selected_templates[1].provider,
|
||||||
|
"model": selected_templates[1].model,
|
||||||
|
"temperature": selected_templates[1].temperature,
|
||||||
|
"max_tokens": selected_templates[1].max_tokens,
|
||||||
|
"prompt": selected_templates[1].prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(template2_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e3",
|
||||||
|
"source": "condition-1",
|
||||||
|
"target": "template-2",
|
||||||
|
"sourceHandle": "true"
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(template_ids) > 2:
|
||||||
|
template3_node = {
|
||||||
|
"id": "template-3",
|
||||||
|
"type": "template",
|
||||||
|
"position": {"x": 700, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": selected_templates[2].name,
|
||||||
|
"template_id": template_ids[2],
|
||||||
|
"provider": selected_templates[2].provider,
|
||||||
|
"model": selected_templates[2].model,
|
||||||
|
"temperature": selected_templates[2].temperature,
|
||||||
|
"max_tokens": selected_templates[2].max_tokens,
|
||||||
|
"prompt": selected_templates[2].prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(template3_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e4",
|
||||||
|
"source": "condition-1",
|
||||||
|
"target": "template-3",
|
||||||
|
"sourceHandle": "false"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 数据转换节点
|
||||||
|
transform_node = {
|
||||||
|
"id": "transform-1",
|
||||||
|
"type": "transform",
|
||||||
|
"position": {"x": 900, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": "数据转换",
|
||||||
|
"mode": "mapping",
|
||||||
|
"mapping": {
|
||||||
|
"final_result": "{{result}}",
|
||||||
|
"processed": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(transform_node)
|
||||||
|
|
||||||
|
# 连接条件分支到转换节点
|
||||||
|
if len(template_ids) > 1:
|
||||||
|
edges.append({
|
||||||
|
"id": "e5",
|
||||||
|
"source": "template-2",
|
||||||
|
"target": "transform-1"
|
||||||
|
})
|
||||||
|
if len(template_ids) > 2:
|
||||||
|
edges.append({
|
||||||
|
"id": "e6",
|
||||||
|
"source": "template-3",
|
||||||
|
"target": "transform-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 最后一个模板节点(如果还有模板)
|
||||||
|
if len(template_ids) > 3:
|
||||||
|
template4_node = {
|
||||||
|
"id": "template-4",
|
||||||
|
"type": "template",
|
||||||
|
"position": {"x": 1100, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": selected_templates[3].name,
|
||||||
|
"template_id": template_ids[3],
|
||||||
|
"provider": selected_templates[3].provider,
|
||||||
|
"model": selected_templates[3].model,
|
||||||
|
"temperature": selected_templates[3].temperature,
|
||||||
|
"max_tokens": selected_templates[3].max_tokens,
|
||||||
|
"prompt": selected_templates[3].prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(template4_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e7",
|
||||||
|
"source": "transform-1",
|
||||||
|
"target": "template-4"
|
||||||
|
})
|
||||||
|
# 结束节点连接到最后一个模板
|
||||||
|
end_node = {
|
||||||
|
"id": "end-1",
|
||||||
|
"type": "end",
|
||||||
|
"position": {"x": 1300, "y": 200},
|
||||||
|
"data": {"label": "结束"}
|
||||||
|
}
|
||||||
|
nodes.append(end_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e8",
|
||||||
|
"source": "template-4",
|
||||||
|
"target": "end-1"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 如果没有更多模板,转换节点直接连接到结束节点
|
||||||
|
end_node = {
|
||||||
|
"id": "end-1",
|
||||||
|
"type": "end",
|
||||||
|
"position": {"x": 1100, "y": 200},
|
||||||
|
"data": {"label": "结束"}
|
||||||
|
}
|
||||||
|
nodes.append(end_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e7",
|
||||||
|
"source": "transform-1",
|
||||||
|
"target": "end-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 检查是否已存在同名Agent
|
||||||
|
agent_name = "复杂模板Agent(多节点)"
|
||||||
|
existing = db.query(Agent).filter(
|
||||||
|
Agent.name == agent_name,
|
||||||
|
Agent.user_id == user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
print(f"⚠️ Agent '{agent_name}' 已存在,将更新它...")
|
||||||
|
existing.workflow_config = {"nodes": nodes, "edges": edges}
|
||||||
|
existing.description = f"包含 {len(selected_templates)} 个节点模板的复杂Agent,支持条件分支和数据转换"
|
||||||
|
existing.updated_at = datetime.now()
|
||||||
|
agent = existing
|
||||||
|
else:
|
||||||
|
# 创建Agent
|
||||||
|
agent = Agent(
|
||||||
|
name=agent_name,
|
||||||
|
description=f"包含 {len(selected_templates)} 个节点模板的复杂Agent,支持条件分支和数据转换",
|
||||||
|
workflow_config={"nodes": nodes, "edges": edges},
|
||||||
|
status="published",
|
||||||
|
user_id=user.id,
|
||||||
|
version=1
|
||||||
|
)
|
||||||
|
db.add(agent)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(agent)
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"✅ 成功创建/更新复杂Agent: {agent.name}")
|
||||||
|
print(f" ID: {agent.id}")
|
||||||
|
print(f" 状态: {agent.status}")
|
||||||
|
print(f" 节点数量: {len(nodes)}")
|
||||||
|
print(f" 连接数量: {len(edges)}")
|
||||||
|
print(f" 使用的模板节点: {len(selected_templates)}")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("工作流结构:")
|
||||||
|
print(" 开始 -> 模板1 -> 条件判断 -> 模板2/模板3 -> 数据转换 -> 模板4 -> 结束")
|
||||||
|
print()
|
||||||
|
return agent
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"❌ 创建失败: {e}")
|
||||||
|
print("=" * 60)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="生成包含多个节点模板的复杂Agent")
|
||||||
|
parser.add_argument(
|
||||||
|
"--username",
|
||||||
|
type=str,
|
||||||
|
default="admin",
|
||||||
|
help="创建Agent的用户名(默认: admin)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
generate_complex_agent_with_templates(db, username=args.username)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 发生错误: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
490
backend/scripts/generate_content_agent.py
Executable file
490
backend/scripts/generate_content_agent.py
Executable file
@@ -0,0 +1,490 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
生成内容生成助手Agent
|
||||||
|
这是一个专门用于内容生成的Agent,能够根据用户需求生成各种类型的内容
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models.agent import Agent
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.node_template import NodeTemplate
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def generate_content_agent(db: Session, username: str = "admin"):
|
||||||
|
"""生成内容生成助手Agent"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("生成内容生成助手Agent")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 查找用户
|
||||||
|
user = db.query(User).filter(User.username == username).first()
|
||||||
|
if not user:
|
||||||
|
print(f"❌ 未找到用户 '{username}',请先创建该用户")
|
||||||
|
return
|
||||||
|
print(f"✅ 找到用户: {user.username} (ID: {user.id})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 生成内容生成工作流配置
|
||||||
|
# 工作流结构:
|
||||||
|
# 开始 -> 需求分析 -> 内容类型判断 -> [文章生成分支 | 文案生成分支 | 脚本生成分支] -> 内容优化 -> 格式检查 -> 结束
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
# 1. 开始节点
|
||||||
|
start_node = {
|
||||||
|
"id": "start-1",
|
||||||
|
"type": "start",
|
||||||
|
"position": {"x": 50, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "开始",
|
||||||
|
"output_format": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(start_node)
|
||||||
|
|
||||||
|
# 2. 需求分析节点(LLM节点)
|
||||||
|
requirement_analysis_node = {
|
||||||
|
"id": "llm-requirement-analysis",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 250, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "需求分析",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.7",
|
||||||
|
"max_tokens": "1500",
|
||||||
|
"prompt": """你是一位专业的内容需求分析师。请分析用户的内容生成需求,提取关键信息。
|
||||||
|
|
||||||
|
用户输入:{{query}}
|
||||||
|
|
||||||
|
请分析并提取以下信息:
|
||||||
|
1. **内容类型**:文章、博客、营销文案、视频脚本、产品描述、社交媒体内容等
|
||||||
|
2. **主题/话题**:用户想要生成什么主题的内容
|
||||||
|
3. **目标受众**:内容面向的读者群体
|
||||||
|
4. **内容风格**:正式、轻松、专业、幽默等
|
||||||
|
5. **字数要求**:大概的字数范围
|
||||||
|
6. **特殊要求**:格式要求、关键词、结构要求等
|
||||||
|
|
||||||
|
请以JSON格式输出分析结果:
|
||||||
|
{
|
||||||
|
"content_type": "内容类型",
|
||||||
|
"topic": "主题",
|
||||||
|
"target_audience": "目标受众",
|
||||||
|
"style": "内容风格",
|
||||||
|
"word_count": "字数要求",
|
||||||
|
"special_requirements": "特殊要求",
|
||||||
|
"keywords": ["关键词1", "关键词2"]
|
||||||
|
}"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(requirement_analysis_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e1",
|
||||||
|
"source": "start-1",
|
||||||
|
"target": "llm-requirement-analysis"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. 内容整合节点(Transform节点)- 用于传递数据
|
||||||
|
content_prepare_node = {
|
||||||
|
"id": "transform-prepare",
|
||||||
|
"type": "transform",
|
||||||
|
"position": {"x": 450, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "准备数据",
|
||||||
|
"mode": "merge",
|
||||||
|
"mapping": {
|
||||||
|
"requirement_analysis": "{{output}}",
|
||||||
|
"query": "{{query}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(content_prepare_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e2",
|
||||||
|
"source": "llm-requirement-analysis",
|
||||||
|
"target": "transform-prepare"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 内容类型判断节点(条件节点)
|
||||||
|
# 简化:直接根据需求分析结果判断,使用LLM输出中的关键词
|
||||||
|
content_type_condition = {
|
||||||
|
"id": "condition-content-type",
|
||||||
|
"type": "condition",
|
||||||
|
"position": {"x": 650, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "内容类型判断",
|
||||||
|
"condition": "{requirement_analysis} contains '文章' or {requirement_analysis} contains '博客' or {requirement_analysis} contains 'article'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(content_type_condition)
|
||||||
|
edges.append({
|
||||||
|
"id": "e2-1",
|
||||||
|
"source": "transform-prepare",
|
||||||
|
"target": "condition-content-type"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 文章生成节点(LLM节点)
|
||||||
|
article_generation_node = {
|
||||||
|
"id": "llm-article-generation",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 650, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": "文章生成",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.8",
|
||||||
|
"max_tokens": "4000",
|
||||||
|
"prompt": """你是一位专业的内容创作专家。请根据用户需求生成一篇高质量的文章。
|
||||||
|
|
||||||
|
**用户原始需求**:{{query}}
|
||||||
|
|
||||||
|
**需求分析结果**:
|
||||||
|
- 主题:{{topic}}
|
||||||
|
- 目标受众:{{target_audience}}
|
||||||
|
- 内容风格:{{style}}
|
||||||
|
- 字数要求:{{word_count}}
|
||||||
|
- 特殊要求:{{special_requirements}}
|
||||||
|
- 关键词:{{keywords}}
|
||||||
|
|
||||||
|
请生成一篇结构完整、内容丰富的文章,要求:
|
||||||
|
1. **标题**:吸引人且准确反映内容
|
||||||
|
2. **引言**:引人入胜的开头,概述文章要点
|
||||||
|
3. **正文**:
|
||||||
|
- 分多个段落,逻辑清晰
|
||||||
|
- 每个段落有明确的主题
|
||||||
|
- 使用适当的过渡词连接段落
|
||||||
|
- 包含具体例子、数据或案例(如适用)
|
||||||
|
4. **结论**:总结要点,给出行动建议或思考
|
||||||
|
5. **格式**:使用Markdown格式,包含标题、列表、加粗等
|
||||||
|
|
||||||
|
**重要**:确保内容完全符合用户需求,不要偏离主题。"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(article_generation_node)
|
||||||
|
|
||||||
|
# 6. 文案生成节点(LLM节点)
|
||||||
|
copywriting_node = {
|
||||||
|
"id": "llm-copywriting",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 850, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "文案生成",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.9",
|
||||||
|
"max_tokens": "2000",
|
||||||
|
"prompt": """你是一位资深的营销文案专家。请根据用户需求生成吸引人的营销文案。
|
||||||
|
|
||||||
|
**用户原始需求**:{{query}}
|
||||||
|
|
||||||
|
**需求分析结果**:
|
||||||
|
- 主题:{{topic}}
|
||||||
|
- 目标受众:{{target_audience}}
|
||||||
|
- 内容风格:{{style}}
|
||||||
|
- 字数要求:{{word_count}}
|
||||||
|
- 特殊要求:{{special_requirements}}
|
||||||
|
- 关键词:{{keywords}}
|
||||||
|
|
||||||
|
请生成营销文案,要求:
|
||||||
|
1. **标题/口号**:简洁有力,吸引眼球
|
||||||
|
2. **核心卖点**:突出产品或服务的核心价值
|
||||||
|
3. **情感共鸣**:触动目标受众的情感
|
||||||
|
4. **行动号召**:明确的行动指引
|
||||||
|
5. **语言风格**:符合目标受众的阅读习惯
|
||||||
|
|
||||||
|
**重要**:确保文案具有说服力和吸引力,能够有效传达信息并促使行动。"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(copywriting_node)
|
||||||
|
|
||||||
|
# 7. 脚本生成节点(LLM节点)
|
||||||
|
script_generation_node = {
|
||||||
|
"id": "llm-script-generation",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 850, "y": 400},
|
||||||
|
"data": {
|
||||||
|
"label": "脚本生成",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.8",
|
||||||
|
"max_tokens": "3000",
|
||||||
|
"prompt": """你是一位专业的视频/音频脚本创作专家。请根据用户需求生成专业的脚本。
|
||||||
|
|
||||||
|
**用户原始需求**:{{query}}
|
||||||
|
|
||||||
|
**需求分析结果**:
|
||||||
|
- 主题:{{topic}}
|
||||||
|
- 目标受众:{{target_audience}}
|
||||||
|
- 内容风格:{{style}}
|
||||||
|
- 字数要求:{{word_count}}
|
||||||
|
- 特殊要求:{{special_requirements}}
|
||||||
|
- 关键词:{{keywords}}
|
||||||
|
|
||||||
|
请生成脚本,要求:
|
||||||
|
1. **开场**:吸引注意力的开头
|
||||||
|
2. **主体内容**:
|
||||||
|
- 分场景/段落
|
||||||
|
- 标注说话者(如适用)
|
||||||
|
- 包含动作提示(如适用)
|
||||||
|
- 时间控制建议
|
||||||
|
3. **结尾**:有力的总结或行动号召
|
||||||
|
4. **格式**:清晰标注场景、角色、动作等
|
||||||
|
|
||||||
|
**重要**:确保脚本结构清晰,适合视频或音频制作。"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(script_generation_node)
|
||||||
|
|
||||||
|
# 8. 通用内容生成节点(兜底)
|
||||||
|
general_generation_node = {
|
||||||
|
"id": "llm-general-generation",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 850, "y": 500},
|
||||||
|
"data": {
|
||||||
|
"label": "通用内容生成",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.8",
|
||||||
|
"max_tokens": "3000",
|
||||||
|
"prompt": """你是一位专业的内容创作专家。请根据用户需求生成高质量的内容。
|
||||||
|
|
||||||
|
**用户原始需求**:{{query}}
|
||||||
|
|
||||||
|
**需求分析结果**:
|
||||||
|
- 内容类型:{{content_type}}
|
||||||
|
- 主题:{{topic}}
|
||||||
|
- 目标受众:{{target_audience}}
|
||||||
|
- 内容风格:{{style}}
|
||||||
|
- 字数要求:{{word_count}}
|
||||||
|
- 特殊要求:{{special_requirements}}
|
||||||
|
- 关键词:{{keywords}}
|
||||||
|
|
||||||
|
请生成符合要求的内容,确保:
|
||||||
|
1. 内容完整、结构清晰
|
||||||
|
2. 符合目标受众的阅读习惯
|
||||||
|
3. 风格与要求一致
|
||||||
|
4. 包含必要的关键词
|
||||||
|
5. 满足字数要求
|
||||||
|
|
||||||
|
**重要**:确保生成的内容完全符合用户需求。"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(general_generation_node)
|
||||||
|
|
||||||
|
# 连接条件节点到各个生成节点
|
||||||
|
edges.append({
|
||||||
|
"id": "e3",
|
||||||
|
"source": "condition-content-type",
|
||||||
|
"target": "llm-article-generation",
|
||||||
|
"sourceHandle": "true",
|
||||||
|
"targetHandle": "input",
|
||||||
|
"condition": "article"
|
||||||
|
})
|
||||||
|
edges.append({
|
||||||
|
"id": "e4",
|
||||||
|
"source": "condition-content-type",
|
||||||
|
"target": "llm-copywriting",
|
||||||
|
"sourceHandle": "true",
|
||||||
|
"targetHandle": "input",
|
||||||
|
"condition": "copywriting"
|
||||||
|
})
|
||||||
|
edges.append({
|
||||||
|
"id": "e5",
|
||||||
|
"source": "condition-content-type",
|
||||||
|
"target": "llm-script-generation",
|
||||||
|
"sourceHandle": "true",
|
||||||
|
"targetHandle": "input",
|
||||||
|
"condition": "script"
|
||||||
|
})
|
||||||
|
edges.append({
|
||||||
|
"id": "e6",
|
||||||
|
"source": "condition-content-type",
|
||||||
|
"target": "llm-general-generation",
|
||||||
|
"sourceHandle": "false",
|
||||||
|
"targetHandle": "input"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 9. 内容整合节点(Transform节点)
|
||||||
|
content_integration_node = {
|
||||||
|
"id": "transform-integration",
|
||||||
|
"type": "transform",
|
||||||
|
"position": {"x": 1050, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "内容整合",
|
||||||
|
"mode": "merge",
|
||||||
|
"mapping": {
|
||||||
|
"generated_content": "{{output}}",
|
||||||
|
"original_query": "{{query}}",
|
||||||
|
"requirement_analysis": "{{requirement_analysis}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(content_integration_node)
|
||||||
|
|
||||||
|
# 连接所有生成节点到整合节点
|
||||||
|
for gen_node_id in ["llm-article-generation", "llm-copywriting", "llm-script-generation", "llm-general-generation"]:
|
||||||
|
edges.append({
|
||||||
|
"id": f"e-{gen_node_id}-integration",
|
||||||
|
"source": gen_node_id,
|
||||||
|
"target": "transform-integration"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 10. 内容优化节点(LLM节点)
|
||||||
|
content_optimization_node = {
|
||||||
|
"id": "llm-optimization",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 1250, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "内容优化",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.7",
|
||||||
|
"max_tokens": "4000",
|
||||||
|
"prompt": """你是一位专业的内容编辑和优化专家。请对生成的内容进行优化和润色。
|
||||||
|
|
||||||
|
**用户原始需求**:{{original_query}}
|
||||||
|
|
||||||
|
**需求分析结果**:{{requirement_analysis}}
|
||||||
|
|
||||||
|
**生成的内容**:{{generated_content}}
|
||||||
|
|
||||||
|
请对内容进行优化,确保:
|
||||||
|
1. **内容质量**:
|
||||||
|
- 检查逻辑是否清晰
|
||||||
|
- 确保信息准确、完整
|
||||||
|
- 优化表达,使其更流畅
|
||||||
|
2. **格式规范**:
|
||||||
|
- 检查Markdown格式是否正确
|
||||||
|
- 确保标题层级合理
|
||||||
|
- 优化段落结构
|
||||||
|
3. **风格一致性**:
|
||||||
|
- 确保风格符合要求
|
||||||
|
- 统一用词和语调
|
||||||
|
4. **用户体验**:
|
||||||
|
- 确保内容易读易懂
|
||||||
|
- 优化可读性
|
||||||
|
|
||||||
|
**输出要求**:
|
||||||
|
- 直接输出优化后的完整内容(纯文本Markdown格式)
|
||||||
|
- 不要包含JSON格式或其他包装
|
||||||
|
- 确保内容完整、专业、易读"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(content_optimization_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e7",
|
||||||
|
"source": "transform-integration",
|
||||||
|
"target": "llm-optimization"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 11. 结束节点
|
||||||
|
end_node = {
|
||||||
|
"id": "end-1",
|
||||||
|
"type": "end",
|
||||||
|
"position": {"x": 1450, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "结束",
|
||||||
|
"output_format": "text" # 默认纯文本格式,适合对话场景
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(end_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e8",
|
||||||
|
"source": "llm-optimization",
|
||||||
|
"target": "end-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 创建或更新Agent
|
||||||
|
workflow_config = {
|
||||||
|
"nodes": nodes,
|
||||||
|
"edges": edges
|
||||||
|
}
|
||||||
|
|
||||||
|
agent = db.query(Agent).filter(
|
||||||
|
Agent.name == "内容生成助手",
|
||||||
|
Agent.user_id == user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if agent:
|
||||||
|
agent.workflow_config = workflow_config
|
||||||
|
agent.updated_at = datetime.now()
|
||||||
|
print("⚠️ Agent '内容生成助手' 已存在,将更新它...")
|
||||||
|
else:
|
||||||
|
agent = Agent(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
name="内容生成助手",
|
||||||
|
description="智能内容生成助手,能够根据用户需求生成各种类型的高质量内容,包括文章、博客、营销文案、视频脚本等。支持需求分析、内容生成、优化润色等完整流程。",
|
||||||
|
workflow_config=workflow_config,
|
||||||
|
status="published",
|
||||||
|
user_id=user.id,
|
||||||
|
version=1
|
||||||
|
)
|
||||||
|
db.add(agent)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(agent)
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"✅ 成功创建/更新内容生成助手Agent")
|
||||||
|
print(f" ID: {agent.id}")
|
||||||
|
print(f" 状态: {agent.status}")
|
||||||
|
print(f" 节点数量: {len(nodes)}")
|
||||||
|
print(f" 连接数量: {len(edges)}")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("工作流结构:")
|
||||||
|
print(" 开始 -> 需求分析 -> 内容类型判断 -> [文章生成 | 文案生成 | 脚本生成 | 通用生成] -> 内容整合 -> 内容优化 -> 结束")
|
||||||
|
print()
|
||||||
|
print("节点说明:")
|
||||||
|
print(" 1. 开始节点:接收用户输入(主题、类型、要求等)")
|
||||||
|
print(" 2. 需求分析节点:分析用户需求,提取内容类型、主题、风格等关键信息")
|
||||||
|
print(" 3. 数据准备节点:整合需求分析结果和原始查询")
|
||||||
|
print(" 4. 内容类型判断节点:根据内容类型进行分支处理")
|
||||||
|
print(" 5. 文章生成节点:生成结构完整的文章内容")
|
||||||
|
print(" 6. 文案生成节点:生成营销文案、广告文案等")
|
||||||
|
print(" 7. 脚本生成节点:生成视频/音频脚本")
|
||||||
|
print(" 8. 通用内容生成节点:处理其他类型的内容生成需求")
|
||||||
|
print(" 9. 内容整合节点:整合生成的内容和需求信息")
|
||||||
|
print(" 10. 内容优化节点:优化和润色内容,确保质量")
|
||||||
|
print(" 11. 结束节点:返回最终优化后的内容")
|
||||||
|
print()
|
||||||
|
print("使用示例:")
|
||||||
|
print(" 输入:'帮我写一篇关于人工智能发展趋势的博客文章,2000字左右,风格轻松易懂'")
|
||||||
|
print(" 输入:'生成一个产品推广的营销文案,面向年轻白领群体'")
|
||||||
|
print(" 输入:'写一个5分钟的产品介绍视频脚本'")
|
||||||
|
print()
|
||||||
|
return agent
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"❌ 创建失败: {e}")
|
||||||
|
print("=" * 60)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="生成内容生成助手Agent")
|
||||||
|
parser.add_argument("--username", type=str, default="admin", help="用户名")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
generate_content_agent(db, username=args.username)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
468
backend/scripts/generate_smart_agent.py
Executable file
468
backend/scripts/generate_smart_agent.py
Executable file
@@ -0,0 +1,468 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
生成智能需求分析与解决方案生成Agent
|
||||||
|
这是一个复杂的多节点Agent,能够理解用户需求并生成专业的解决方案
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.database import SessionLocal
|
||||||
|
from app.models.agent import Agent
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.node_template import NodeTemplate
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def generate_smart_agent(db: Session, username: str = "admin"):
|
||||||
|
"""生成智能需求分析与解决方案生成Agent"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("生成智能需求分析与解决方案生成Agent")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 查找用户
|
||||||
|
user = db.query(User).filter(User.username == username).first()
|
||||||
|
if not user:
|
||||||
|
print(f"❌ 未找到用户 '{username}',请先创建该用户")
|
||||||
|
return
|
||||||
|
print(f"✅ 找到用户: {user.username} (ID: {user.id})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 查找可用的节点模板(优先选择特定类型的模板)
|
||||||
|
# 优先查找:工作流设计、API集成、数据分析、业务流程等类型的模板
|
||||||
|
preferred_categories = ["工作流设计", "API集成", "数据分析", "业务流程", "技术方案"]
|
||||||
|
|
||||||
|
templates = []
|
||||||
|
for category in preferred_categories:
|
||||||
|
category_templates = db.query(NodeTemplate).filter(
|
||||||
|
((NodeTemplate.is_public == True) | (NodeTemplate.user_id == user.id)) &
|
||||||
|
(NodeTemplate.category == category)
|
||||||
|
).limit(2).all()
|
||||||
|
templates.extend(category_templates)
|
||||||
|
|
||||||
|
# 如果没找到足够的模板,补充其他公开模板
|
||||||
|
if len(templates) < 3:
|
||||||
|
other_templates = db.query(NodeTemplate).filter(
|
||||||
|
(NodeTemplate.is_public == True) | (NodeTemplate.user_id == user.id)
|
||||||
|
).limit(5).all()
|
||||||
|
for t in other_templates:
|
||||||
|
if t not in templates:
|
||||||
|
templates.append(t)
|
||||||
|
if len(templates) >= 5:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(templates) < 2:
|
||||||
|
print(f"⚠️ 警告: 只找到 {len(templates)} 个节点模板,将使用LLM节点代替")
|
||||||
|
use_templates = False
|
||||||
|
else:
|
||||||
|
use_templates = True
|
||||||
|
print(f"📋 找到 {len(templates)} 个可用节点模板")
|
||||||
|
for i, template in enumerate(templates[:5], 1):
|
||||||
|
print(f" {i}. {template.name} ({template.category})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 生成智能工作流配置
|
||||||
|
# 工作流结构:
|
||||||
|
# 开始 -> 需求理解 -> 需求分类 -> [技术方案分支 | 业务流程分支 | 数据分析分支] -> 方案整合 -> 输出优化 -> 结束
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
|
||||||
|
# 1. 开始节点
|
||||||
|
start_node = {
|
||||||
|
"id": "start-1",
|
||||||
|
"type": "start",
|
||||||
|
"position": {"x": 50, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "开始",
|
||||||
|
"output_format": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(start_node)
|
||||||
|
|
||||||
|
# 2. 需求理解节点(LLM节点)
|
||||||
|
requirement_analysis_node = {
|
||||||
|
"id": "llm-requirement-analysis",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 250, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "需求理解与分析",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.7",
|
||||||
|
"max_tokens": "2000",
|
||||||
|
"prompt": """你是一位专业的需求分析师。请分析用户的需求,并提取关键信息。
|
||||||
|
|
||||||
|
用户需求:{input}
|
||||||
|
|
||||||
|
请按照以下格式输出JSON:
|
||||||
|
{{
|
||||||
|
"requirement_type": "技术方案|业务流程|数据分析|工作流设计|其他",
|
||||||
|
"key_points": ["关键点1", "关键点2", ...],
|
||||||
|
"complexity": "简单|中等|复杂",
|
||||||
|
"domain": "领域(如:电商、金融、教育等)",
|
||||||
|
"summary": "需求摘要"
|
||||||
|
}}"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(requirement_analysis_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e1",
|
||||||
|
"source": "start-1",
|
||||||
|
"target": "llm-requirement-analysis"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. 需求分类节点(条件节点)
|
||||||
|
classification_node = {
|
||||||
|
"id": "condition-classify",
|
||||||
|
"type": "condition",
|
||||||
|
"position": {"x": 450, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "需求分类",
|
||||||
|
"condition": "{{requirement_type}} == '技术方案' or {{requirement_type}} == 'API集成'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(classification_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e2",
|
||||||
|
"source": "llm-requirement-analysis",
|
||||||
|
"target": "condition-classify"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 技术方案分支(如果使用模板)
|
||||||
|
if use_templates and len(templates) > 0:
|
||||||
|
# 查找技术方案相关的模板
|
||||||
|
tech_template = None
|
||||||
|
for t in templates:
|
||||||
|
if "技术" in t.category or "API" in t.category or "集成" in t.name:
|
||||||
|
tech_template = t
|
||||||
|
break
|
||||||
|
if not tech_template:
|
||||||
|
tech_template = templates[0]
|
||||||
|
|
||||||
|
tech_solution_node = {
|
||||||
|
"id": "template-tech-solution",
|
||||||
|
"type": "template",
|
||||||
|
"position": {"x": 650, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": tech_template.name,
|
||||||
|
"template_id": tech_template.id,
|
||||||
|
"provider": tech_template.provider or "deepseek",
|
||||||
|
"model": tech_template.model or "deepseek-chat",
|
||||||
|
"temperature": str(tech_template.temperature) if tech_template.temperature else "0.7",
|
||||||
|
"max_tokens": str(tech_template.max_tokens) if tech_template.max_tokens else "2000",
|
||||||
|
"prompt": tech_template.prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(tech_solution_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e3",
|
||||||
|
"source": "condition-classify",
|
||||||
|
"target": "template-tech-solution",
|
||||||
|
"sourceHandle": "true"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 使用LLM节点代替
|
||||||
|
tech_solution_node = {
|
||||||
|
"id": "llm-tech-solution",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 650, "y": 200},
|
||||||
|
"data": {
|
||||||
|
"label": "技术方案设计",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.7",
|
||||||
|
"max_tokens": "3000",
|
||||||
|
"prompt": """你是一位资深的技术架构师。根据需求分析结果,设计一个完整的技术方案。
|
||||||
|
|
||||||
|
需求分析结果:{{requirement_analysis}}
|
||||||
|
|
||||||
|
请提供:
|
||||||
|
1. 技术选型建议
|
||||||
|
2. 架构设计
|
||||||
|
3. 实施步骤
|
||||||
|
4. 风险评估
|
||||||
|
5. 最佳实践建议
|
||||||
|
|
||||||
|
输出格式:结构化的Markdown文档"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(tech_solution_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e3",
|
||||||
|
"source": "condition-classify",
|
||||||
|
"target": "llm-tech-solution",
|
||||||
|
"sourceHandle": "true"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5. 业务流程分支(优先使用LLM节点,确保能理解用户需求)
|
||||||
|
# 使用LLM节点而不是模板节点,因为LLM节点可以更好地理解需求上下文
|
||||||
|
process_solution_node = {
|
||||||
|
"id": "llm-process-solution",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 650, "y": 400},
|
||||||
|
"data": {
|
||||||
|
"label": "业务流程设计",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.7",
|
||||||
|
"max_tokens": "4000",
|
||||||
|
"prompt": """你是一位资深的业务流程设计专家。请根据用户的需求,设计一个完整、详细的业务流程方案。
|
||||||
|
|
||||||
|
**重要**:请仔细阅读用户原始需求,确保设计方案完全符合用户的具体需求。
|
||||||
|
|
||||||
|
用户原始需求:{{query}}
|
||||||
|
|
||||||
|
需求分析结果:{{requirement_analysis}}
|
||||||
|
|
||||||
|
请提供以下内容:
|
||||||
|
1. **流程概述**:整体流程的目标、范围和价值
|
||||||
|
2. **详细流程设计**:
|
||||||
|
- 流程的各个阶段和环节
|
||||||
|
- 每个环节的输入、输出和处理逻辑
|
||||||
|
- 状态流转图(用Mermaid或文字描述)
|
||||||
|
3. **关键步骤说明**:
|
||||||
|
- 每个步骤的具体操作
|
||||||
|
- 前置条件和后置条件
|
||||||
|
- 异常情况处理
|
||||||
|
4. **角色与职责**:
|
||||||
|
- 涉及的角色/系统
|
||||||
|
- 每个角色的职责和权限
|
||||||
|
5. **数据流转**:
|
||||||
|
- 关键数据在各环节的流转
|
||||||
|
- 数据格式和验证规则
|
||||||
|
6. **异常处理机制**:
|
||||||
|
- 常见异常场景
|
||||||
|
- 异常处理流程
|
||||||
|
7. **流程优化建议**:
|
||||||
|
- 性能优化点
|
||||||
|
- 用户体验优化
|
||||||
|
8. **实施路线图**:
|
||||||
|
- 分阶段实施计划
|
||||||
|
- 关键里程碑
|
||||||
|
|
||||||
|
**重要**:请确保设计方案完全符合用户的需求,不要偏离主题。如果用户需求是电商订单流程,就设计电商订单流程;如果是其他业务,就设计对应的业务流程。
|
||||||
|
|
||||||
|
输出格式:结构化的Markdown文档,使用清晰的标题和列表"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(process_solution_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e4",
|
||||||
|
"source": "condition-classify",
|
||||||
|
"target": "llm-process-solution",
|
||||||
|
"sourceHandle": "false"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 6. 方案整合节点(Transform节点)
|
||||||
|
integration_node = {
|
||||||
|
"id": "transform-integration",
|
||||||
|
"type": "transform",
|
||||||
|
"position": {"x": 850, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "方案整合",
|
||||||
|
"mode": "merge",
|
||||||
|
"mapping": {
|
||||||
|
"solution": "{{result}}",
|
||||||
|
"requirement_analysis": "{{requirement_analysis}}",
|
||||||
|
"query": "{{query}}",
|
||||||
|
"timestamp": "{{timestamp}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(integration_node)
|
||||||
|
|
||||||
|
# 连接分支到整合节点
|
||||||
|
if use_templates and len(templates) > 0:
|
||||||
|
edges.append({
|
||||||
|
"id": "e5",
|
||||||
|
"source": "template-tech-solution" if use_templates else "llm-tech-solution",
|
||||||
|
"target": "transform-integration"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
edges.append({
|
||||||
|
"id": "e5",
|
||||||
|
"source": "llm-tech-solution",
|
||||||
|
"target": "transform-integration"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 业务流程分支始终使用LLM节点
|
||||||
|
edges.append({
|
||||||
|
"id": "e6",
|
||||||
|
"source": "llm-process-solution",
|
||||||
|
"target": "transform-integration"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 7. 输出优化节点(LLM节点)
|
||||||
|
optimization_node = {
|
||||||
|
"id": "llm-optimization",
|
||||||
|
"type": "llm",
|
||||||
|
"position": {"x": 1050, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "输出优化与格式化",
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"temperature": "0.5",
|
||||||
|
"max_tokens": "4000",
|
||||||
|
"prompt": """你是一位专业的技术文档编辑。请对方案进行优化和格式化,确保方案完全符合用户需求。
|
||||||
|
|
||||||
|
用户原始需求:{{query}}
|
||||||
|
需求分析结果:{{requirement_analysis}}
|
||||||
|
整合后的方案:{{solution}}
|
||||||
|
|
||||||
|
**重要检查**:
|
||||||
|
1. 确保方案内容与用户需求完全匹配
|
||||||
|
2. 如果方案偏离了用户需求,请重新生成符合需求的方案
|
||||||
|
3. 如果方案是关于数据清洗的,但用户需求是业务流程设计,请重新生成业务流程设计方案
|
||||||
|
|
||||||
|
请:
|
||||||
|
1. 检查方案是否完全符合用户需求
|
||||||
|
2. 优化文档结构,使其更清晰
|
||||||
|
3. 补充关键细节和实施建议
|
||||||
|
4. 确保格式统一、专业
|
||||||
|
5. 如果发现方案不符合需求,请重新生成正确的方案
|
||||||
|
|
||||||
|
**输出要求**:
|
||||||
|
- 只输出纯文本的Markdown文档内容
|
||||||
|
- 不要包含任何JSON格式、代码块标记或其他格式包装
|
||||||
|
- 直接输出方案文档的正文内容
|
||||||
|
- 确保内容完整、专业、易读
|
||||||
|
|
||||||
|
输出格式:直接输出完整的Markdown文档正文(纯文本)"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(optimization_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e7",
|
||||||
|
"source": "transform-integration",
|
||||||
|
"target": "llm-optimization"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 8. 结束节点
|
||||||
|
end_node = {
|
||||||
|
"id": "end-1",
|
||||||
|
"type": "end",
|
||||||
|
"position": {"x": 1250, "y": 300},
|
||||||
|
"data": {
|
||||||
|
"label": "结束",
|
||||||
|
"output_format": "text" # 默认纯文本格式,适合对话场景
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes.append(end_node)
|
||||||
|
edges.append({
|
||||||
|
"id": "e8",
|
||||||
|
"source": "llm-optimization",
|
||||||
|
"target": "end-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 检查是否已存在同名Agent
|
||||||
|
agent_name = "智能需求分析与解决方案生成器"
|
||||||
|
existing = db.query(Agent).filter(
|
||||||
|
Agent.name == agent_name,
|
||||||
|
Agent.user_id == user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
print(f"⚠️ Agent '{agent_name}' 已存在,将更新它...")
|
||||||
|
existing.workflow_config = {"nodes": nodes, "edges": edges}
|
||||||
|
existing.description = """智能需求分析与解决方案生成Agent
|
||||||
|
|
||||||
|
功能特点:
|
||||||
|
1. 自动理解用户需求并提取关键信息
|
||||||
|
2. 根据需求类型智能分类(技术方案/业务流程/数据分析等)
|
||||||
|
3. 调用专业模板或LLM生成针对性解决方案
|
||||||
|
4. 整合并优化输出,生成专业文档
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
- 技术方案设计
|
||||||
|
- 业务流程优化
|
||||||
|
- 系统架构设计
|
||||||
|
- 问题分析与解决"""
|
||||||
|
existing.updated_at = datetime.now()
|
||||||
|
agent = existing
|
||||||
|
else:
|
||||||
|
# 创建Agent
|
||||||
|
agent = Agent(
|
||||||
|
name=agent_name,
|
||||||
|
description="""智能需求分析与解决方案生成Agent
|
||||||
|
|
||||||
|
功能特点:
|
||||||
|
1. 自动理解用户需求并提取关键信息
|
||||||
|
2. 根据需求类型智能分类(技术方案/业务流程/数据分析等)
|
||||||
|
3. 调用专业模板或LLM生成针对性解决方案
|
||||||
|
4. 整合并优化输出,生成专业文档
|
||||||
|
|
||||||
|
适用场景:
|
||||||
|
- 技术方案设计
|
||||||
|
- 业务流程优化
|
||||||
|
- 系统架构设计
|
||||||
|
- 问题分析与解决""",
|
||||||
|
workflow_config={"nodes": nodes, "edges": edges},
|
||||||
|
status="published", # 直接发布,可以直接使用
|
||||||
|
user_id=user.id,
|
||||||
|
version=1
|
||||||
|
)
|
||||||
|
db.add(agent)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(agent)
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"✅ 成功创建/更新智能Agent: {agent.name}")
|
||||||
|
print(f" ID: {agent.id}")
|
||||||
|
print(f" 状态: {agent.status}")
|
||||||
|
print(f" 节点数量: {len(nodes)}")
|
||||||
|
print(f" 连接数量: {len(edges)}")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("工作流结构:")
|
||||||
|
print(" 开始 -> 需求理解 -> 需求分类 -> [技术方案分支 | 业务流程分支] -> 方案整合 -> 输出优化 -> 结束")
|
||||||
|
print()
|
||||||
|
print("节点说明:")
|
||||||
|
print(" 1. 开始节点:接收用户输入")
|
||||||
|
print(" 2. 需求理解节点:分析用户需求,提取关键信息")
|
||||||
|
print(" 3. 需求分类节点:根据需求类型进行分支")
|
||||||
|
print(" 4. 技术方案/业务流程节点:生成针对性解决方案")
|
||||||
|
print(" 5. 方案整合节点:整合各分支结果")
|
||||||
|
print(" 6. 输出优化节点:优化和格式化最终输出")
|
||||||
|
print(" 7. 结束节点:返回最终结果")
|
||||||
|
print()
|
||||||
|
return agent
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"❌ 创建失败: {e}")
|
||||||
|
print("=" * 60)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="生成智能需求分析与解决方案生成Agent")
|
||||||
|
parser.add_argument(
|
||||||
|
"--username",
|
||||||
|
type=str,
|
||||||
|
default="admin",
|
||||||
|
help="创建Agent的用户名(默认: admin)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
generate_smart_agent(db, username=args.username)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 发生错误: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick, onUnmounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
UserFilled,
|
UserFilled,
|
||||||
@@ -170,10 +170,15 @@ const props = defineProps<{
|
|||||||
nodeTestResult?: any
|
nodeTestResult?: any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'execution-status': [status: any]
|
||||||
|
}>()
|
||||||
|
|
||||||
const messages = ref<Message[]>([])
|
const messages = ref<Message[]>([])
|
||||||
const inputMessage = ref('')
|
const inputMessage = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const messagesContainer = ref<HTMLElement>()
|
const messagesContainer = ref<HTMLElement>()
|
||||||
|
let pollingInterval: any = null
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
@@ -208,8 +213,16 @@ const handleSendMessage = async () => {
|
|||||||
// 轮询执行状态
|
// 轮询执行状态
|
||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const statusResponse = await api.get(`/api/v1/executions/${execution.id}`)
|
// 获取详细执行状态(包含节点执行信息)
|
||||||
const exec = statusResponse.data
|
const statusResponse = await api.get(`/api/v1/executions/${execution.id}/status`)
|
||||||
|
const status = statusResponse.data
|
||||||
|
|
||||||
|
// 将执行状态传递给父组件,用于显示工作流动画
|
||||||
|
emit('execution-status', status)
|
||||||
|
|
||||||
|
// 获取执行详情(用于提取输出结果)
|
||||||
|
const execResponse = await api.get(`/api/v1/executions/${execution.id}`)
|
||||||
|
const exec = execResponse.data
|
||||||
|
|
||||||
if (exec.status === 'completed') {
|
if (exec.status === 'completed') {
|
||||||
// 提取Agent回复
|
// 提取Agent回复
|
||||||
@@ -244,6 +257,16 @@ const handleSendMessage = async () => {
|
|||||||
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
// 延迟清除执行状态,让用户能看到最终的执行结果
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('execution-status', null)
|
||||||
|
}, 3000) // 3秒后清除
|
||||||
|
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
} else if (exec.status === 'failed') {
|
} else if (exec.status === 'failed') {
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
role: 'agent',
|
role: 'agent',
|
||||||
@@ -252,9 +275,19 @@ const handleSendMessage = async () => {
|
|||||||
})
|
})
|
||||||
loading.value = false
|
loading.value = false
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
// 延迟清除执行状态,让用户能看到失败节点的状态
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('execution-status', null)
|
||||||
|
}, 5000) // 5秒后清除
|
||||||
|
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 继续轮询
|
// 继续轮询(pending 或 running 状态)
|
||||||
setTimeout(checkStatus, 1000)
|
// 不需要做任何操作,等待下次轮询
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
@@ -264,11 +297,21 @@ const handleSendMessage = async () => {
|
|||||||
})
|
})
|
||||||
loading.value = false
|
loading.value = false
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
// 清除执行状态
|
||||||
|
emit('execution-status', null)
|
||||||
|
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始轮询
|
// 使用 setInterval 进行轮询,每500毫秒检查一次(更频繁,能捕获快速执行的节点)
|
||||||
setTimeout(checkStatus, 1000)
|
pollingInterval = setInterval(checkStatus, 500)
|
||||||
|
// 立即执行一次
|
||||||
|
checkStatus()
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('发送消息失败:', error)
|
console.error('发送消息失败:', error)
|
||||||
@@ -292,6 +335,13 @@ const handlePresetQuestion = (question: string) => {
|
|||||||
// 清空对话
|
// 清空对话
|
||||||
const handleClearChat = () => {
|
const handleClearChat = () => {
|
||||||
messages.value = []
|
messages.value = []
|
||||||
|
// 清除执行状态
|
||||||
|
emit('execution-status', null)
|
||||||
|
// 清除轮询
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
@@ -341,6 +391,16 @@ const handleCloseNodeTest = () => {
|
|||||||
watch(messages, () => {
|
watch(messages, () => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 组件卸载时清理轮询
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
pollingInterval = null
|
||||||
|
}
|
||||||
|
// 清除执行状态
|
||||||
|
emit('execution-status', null)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -78,21 +78,82 @@ export const StartNode = defineComponent({
|
|||||||
height: '8px'
|
height: '8px'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
// 状态指示器(右上角)
|
||||||
|
isExecuting ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#409eff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
h('div', {
|
||||||
|
class: 'node-loading-spinner',
|
||||||
|
style: {
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||||
|
borderTop: '2px solid white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 0.8s linear infinite'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]) : null,
|
||||||
|
isExecuted ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#67c23a',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✓') : null,
|
||||||
|
isFailed ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#f56c6c',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✕') : null,
|
||||||
h('div', {
|
h('div', {
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '4px'
|
gap: '4px',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
props.data.label || '开始',
|
props.data.label || '开始'
|
||||||
isFailed ? h('span', {
|
|
||||||
style: {
|
|
||||||
fontSize: '12px',
|
|
||||||
marginLeft: '4px'
|
|
||||||
}
|
|
||||||
}, '❌') : null
|
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -171,21 +232,82 @@ export const LLMNode = defineComponent({
|
|||||||
height: '8px'
|
height: '8px'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
// 状态指示器(右上角)
|
||||||
|
isExecuting ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#409eff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
h('div', {
|
||||||
|
class: 'node-loading-spinner',
|
||||||
|
style: {
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||||
|
borderTop: '2px solid white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 0.8s linear infinite'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]) : null,
|
||||||
|
isExecuted ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#67c23a',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✓') : null,
|
||||||
|
isFailed ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#f56c6c',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✕') : null,
|
||||||
h('div', {
|
h('div', {
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '4px'
|
gap: '4px',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
props.data.label || 'LLM',
|
props.data.label || 'LLM'
|
||||||
isFailed ? h('span', {
|
|
||||||
style: {
|
|
||||||
fontSize: '12px',
|
|
||||||
marginLeft: '4px'
|
|
||||||
}
|
|
||||||
}, '❌') : null
|
|
||||||
]),
|
]),
|
||||||
h(Handle, {
|
h(Handle, {
|
||||||
type: 'source',
|
type: 'source',
|
||||||
@@ -282,7 +404,77 @@ export const ConditionNode = defineComponent({
|
|||||||
height: '8px'
|
height: '8px'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
props.data.label || '条件',
|
// 状态指示器(右上角)
|
||||||
|
isExecuting ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#409eff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
h('div', {
|
||||||
|
class: 'node-loading-spinner',
|
||||||
|
style: {
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||||
|
borderTop: '2px solid white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 0.8s linear infinite'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]) : null,
|
||||||
|
isExecuted ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#67c23a',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✓') : null,
|
||||||
|
isFailed ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#f56c6c',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✕') : null,
|
||||||
|
h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1
|
||||||
|
}
|
||||||
|
}, props.data.label || '条件'),
|
||||||
h(Handle, {
|
h(Handle, {
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
@@ -390,7 +582,77 @@ export const EndNode = defineComponent({
|
|||||||
height: '8px'
|
height: '8px'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
props.data.label || '结束'
|
// 状态指示器(右上角)
|
||||||
|
isExecuting ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#409eff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
h('div', {
|
||||||
|
class: 'node-loading-spinner',
|
||||||
|
style: {
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||||
|
borderTop: '2px solid white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 0.8s linear infinite'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]) : null,
|
||||||
|
isExecuted ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#67c23a',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✓') : null,
|
||||||
|
isFailed ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#f56c6c',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✕') : null,
|
||||||
|
h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1
|
||||||
|
}
|
||||||
|
}, props.data.label || '结束')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,7 +728,77 @@ export const DefaultNode = defineComponent({
|
|||||||
height: '8px'
|
height: '8px'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
props.data.label || '节点',
|
// 状态指示器(右上角)
|
||||||
|
isExecuting ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#409eff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
h('div', {
|
||||||
|
class: 'node-loading-spinner',
|
||||||
|
style: {
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||||
|
borderTop: '2px solid white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 0.8s linear infinite'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]) : null,
|
||||||
|
isExecuted ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#67c23a',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✓') : null,
|
||||||
|
isFailed ? h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
right: '-8px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
background: '#f56c6c',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||||
|
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}, '✕') : null,
|
||||||
|
h('div', {
|
||||||
|
style: {
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1
|
||||||
|
}
|
||||||
|
}, props.data.label || '节点'),
|
||||||
h(Handle, {
|
h(Handle, {
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleRun">运行</el-button>
|
<el-button @click="handleRun">运行</el-button>
|
||||||
<el-button @click="handleClear">清空</el-button>
|
<el-button @click="handleClear">清空</el-button>
|
||||||
|
<el-button type="warning" @click="handleTestAnimation" :loading="testingAnimation">
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
{{ testingAnimation ? '测试中...' : '测试动画' }}
|
||||||
|
</el-button>
|
||||||
<el-button v-if="copiedNode" @click="handlePasteNodeFromButton" type="success">
|
<el-button v-if="copiedNode" @click="handlePasteNodeFromButton" type="success">
|
||||||
<el-icon><DocumentCopy /></el-icon>
|
<el-icon><DocumentCopy /></el-icon>
|
||||||
粘贴节点 (Ctrl+V)
|
粘贴节点 (Ctrl+V)
|
||||||
@@ -1003,6 +1007,37 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 结束节点配置 -->
|
||||||
|
<template v-if="selectedNode.type === 'end' || selectedNode.type === 'output'">
|
||||||
|
<el-form-item label="输出格式">
|
||||||
|
<el-select v-model="selectedNode.data.output_format" placeholder="选择输出格式">
|
||||||
|
<el-option label="纯文本(适合对话)" value="text" />
|
||||||
|
<el-option label="JSON格式" value="json" />
|
||||||
|
</el-select>
|
||||||
|
<el-alert
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div style="font-size: 12px;">
|
||||||
|
<strong>纯文本</strong>:自动提取文本内容,适合对话场景<br/>
|
||||||
|
<strong>JSON格式</strong>:保留完整数据结构,适合API调用
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input
|
||||||
|
v-model="selectedNode.data.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="节点描述(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 定时任务节点配置 -->
|
<!-- 定时任务节点配置 -->
|
||||||
<template v-if="isScheduleNodeSelected">
|
<template v-if="isScheduleNodeSelected">
|
||||||
<el-form-item label="延迟类型">
|
<el-form-item label="延迟类型">
|
||||||
@@ -1314,6 +1349,8 @@ const handleManageTemplates = () => {
|
|||||||
const executedNodeIds = ref<Set<string>>(new Set())
|
const executedNodeIds = ref<Set<string>>(new Set())
|
||||||
const runningNodeId = ref<string | null>(null)
|
const runningNodeId = ref<string | null>(null)
|
||||||
const failedNodeIds = ref<Set<string>>(new Set())
|
const failedNodeIds = ref<Set<string>>(new Set())
|
||||||
|
const testingAnimation = ref(false)
|
||||||
|
const testAnimationTimer = ref<number | null>(null)
|
||||||
|
|
||||||
// 运行对话框
|
// 运行对话框
|
||||||
const runDialogVisible = ref(false)
|
const runDialogVisible = ref(false)
|
||||||
@@ -1332,6 +1369,7 @@ const {
|
|||||||
removeNodes,
|
removeNodes,
|
||||||
removeEdges,
|
removeEdges,
|
||||||
updateNode,
|
updateNode,
|
||||||
|
updateEdge,
|
||||||
screenToFlowCoordinate,
|
screenToFlowCoordinate,
|
||||||
zoomIn: vueFlowZoomIn,
|
zoomIn: vueFlowZoomIn,
|
||||||
zoomOut: vueFlowZoomOut,
|
zoomOut: vueFlowZoomOut,
|
||||||
@@ -1558,6 +1596,10 @@ const handleDrop = (event: DragEvent) => {
|
|||||||
body_type: 'text',
|
body_type: 'text',
|
||||||
attachments: []
|
attachments: []
|
||||||
} : {}),
|
} : {}),
|
||||||
|
// 结束节点默认配置
|
||||||
|
...(nodeType === 'end' || nodeType === 'output' ? {
|
||||||
|
output_format: 'text' // 默认纯文本格式,适合对话场景
|
||||||
|
} : {}),
|
||||||
// 消息队列节点默认配置
|
// 消息队列节点默认配置
|
||||||
...(isMQNode ? {
|
...(isMQNode ? {
|
||||||
queue_type: 'rabbitmq',
|
queue_type: 'rabbitmq',
|
||||||
@@ -2385,6 +2427,211 @@ const handleClear = () => {
|
|||||||
ElMessage.success('画布已清空')
|
ElMessage.success('画布已清空')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试动画 - 依次执行所有节点,展示完整的工作流动画效果
|
||||||
|
const handleTestAnimation = () => {
|
||||||
|
// 清除之前的测试状态
|
||||||
|
if (testAnimationTimer.value) {
|
||||||
|
clearTimeout(testAnimationTimer.value)
|
||||||
|
testAnimationTimer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到第一个开始节点
|
||||||
|
const startNode = nodes.value.find(n => n.type === 'start' || n.data?.type === 'start' || n.id.startsWith('start'))
|
||||||
|
|
||||||
|
if (!startNode) {
|
||||||
|
ElMessage.warning('未找到开始节点,请先添加一个开始节点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[rjb] 🧪 开始测试动画,节点ID:', startNode.id)
|
||||||
|
|
||||||
|
// 清除所有节点的执行状态
|
||||||
|
nodes.value.forEach(node => {
|
||||||
|
const nodeClass = (node.class || '').replace(/\b(executing|executed|failed)\b/g, '').trim()
|
||||||
|
if (nodeClass !== node.class) {
|
||||||
|
updateNode(node.id, {
|
||||||
|
class: nodeClass,
|
||||||
|
data: { ...node.data, executionStatus: undefined, executionClass: undefined }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清除所有边的执行状态
|
||||||
|
edges.value.forEach(edge => {
|
||||||
|
const edgeClass = (edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim()
|
||||||
|
if (edgeClass !== edge.class) {
|
||||||
|
if (updateEdge) {
|
||||||
|
updateEdge(edge.id, {
|
||||||
|
class: edgeClass,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#409eff',
|
||||||
|
strokeWidth: 2.5
|
||||||
|
},
|
||||||
|
animated: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
edge.class = edgeClass
|
||||||
|
edge.animated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用广度优先搜索(BFS)找到所有节点的执行顺序
|
||||||
|
const getExecutionOrder = (startNodeId: string): string[] => {
|
||||||
|
const visited = new Set<string>()
|
||||||
|
const order: string[] = []
|
||||||
|
const queue: string[] = [startNodeId]
|
||||||
|
|
||||||
|
visited.add(startNodeId)
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentNodeId = queue.shift()!
|
||||||
|
order.push(currentNodeId)
|
||||||
|
|
||||||
|
// 找到所有从当前节点出发的边
|
||||||
|
const outgoingEdges = edges.value.filter(e => e.source === currentNodeId)
|
||||||
|
for (const edge of outgoingEdges) {
|
||||||
|
if (!visited.has(edge.target)) {
|
||||||
|
visited.add(edge.target)
|
||||||
|
queue.push(edge.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还有未访问的节点(可能是孤立的),也加入顺序
|
||||||
|
nodes.value.forEach(node => {
|
||||||
|
if (!visited.has(node.id)) {
|
||||||
|
order.push(node.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionOrder = getExecutionOrder(startNode.id)
|
||||||
|
console.log('[rjb] 🧪 节点执行顺序:', executionOrder)
|
||||||
|
|
||||||
|
if (executionOrder.length === 0) {
|
||||||
|
ElMessage.warning('未找到可执行的节点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testingAnimation.value = true
|
||||||
|
|
||||||
|
// 依次执行每个节点
|
||||||
|
const executeNodeAnimation = (nodeIndex: number) => {
|
||||||
|
if (nodeIndex >= executionOrder.length) {
|
||||||
|
// 所有节点执行完成
|
||||||
|
testingAnimation.value = false
|
||||||
|
testAnimationTimer.value = null
|
||||||
|
ElMessage.success(`动画测试完成!共执行了 ${executionOrder.length} 个节点`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeId = executionOrder[nodeIndex]
|
||||||
|
const node = nodes.value.find(n => n.id === nodeId)
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
// 如果节点不存在,跳过
|
||||||
|
executeNodeAnimation(nodeIndex + 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[rjb] 🧪 [${nodeIndex + 1}/${executionOrder.length}] 开始执行节点:`, nodeId)
|
||||||
|
|
||||||
|
// 第一步:标记为执行中
|
||||||
|
const nodeClass = ((node.class || '').replace(/\b(executing|executed|failed)\b/g, '').trim() + ' executing').trim()
|
||||||
|
const nodeData = { ...node.data, executionStatus: 'executing', executionClass: 'executing' }
|
||||||
|
updateNode(node.id, {
|
||||||
|
class: nodeClass,
|
||||||
|
data: nodeData
|
||||||
|
})
|
||||||
|
console.log('[rjb] 🧪 ✅ 标记节点为执行中:', nodeId)
|
||||||
|
|
||||||
|
// 高亮连接到当前节点的边(从上游节点来的边)
|
||||||
|
edges.value.forEach(edge => {
|
||||||
|
if (edge.target === nodeId) {
|
||||||
|
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executing').trim()
|
||||||
|
try {
|
||||||
|
if (updateEdge) {
|
||||||
|
updateEdge(edge.id, {
|
||||||
|
class: edgeClass,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#409eff',
|
||||||
|
strokeWidth: 3.5,
|
||||||
|
strokeDasharray: '8,4'
|
||||||
|
},
|
||||||
|
animated: true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
edge.class = edgeClass
|
||||||
|
edge.style = {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#409eff',
|
||||||
|
strokeWidth: 3.5,
|
||||||
|
strokeDasharray: '8,4'
|
||||||
|
}
|
||||||
|
edge.animated = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[rjb] Failed to update edge:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 第二步:1.5秒后标记为已完成,然后执行下一个节点
|
||||||
|
testAnimationTimer.value = window.setTimeout(() => {
|
||||||
|
const executedNodeClass = ((node.class || '').replace(/\b(executing|executed|failed)\b/g, '').trim() + ' executed').trim()
|
||||||
|
const executedNodeData = { ...node.data, executionStatus: 'executed', executionClass: 'executed' }
|
||||||
|
updateNode(node.id, {
|
||||||
|
class: executedNodeClass,
|
||||||
|
data: executedNodeData
|
||||||
|
})
|
||||||
|
console.log('[rjb] 🧪 ✅ 标记节点为已完成:', nodeId)
|
||||||
|
|
||||||
|
// 更新连接到当前节点的边为已完成状态
|
||||||
|
edges.value.forEach(edge => {
|
||||||
|
if (edge.target === nodeId) {
|
||||||
|
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executed').trim()
|
||||||
|
try {
|
||||||
|
if (updateEdge) {
|
||||||
|
updateEdge(edge.id, {
|
||||||
|
class: edgeClass,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#67c23a',
|
||||||
|
strokeWidth: 3,
|
||||||
|
strokeDasharray: '0'
|
||||||
|
},
|
||||||
|
animated: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
edge.class = edgeClass
|
||||||
|
edge.style = {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#67c23a',
|
||||||
|
strokeWidth: 3,
|
||||||
|
strokeDasharray: '0'
|
||||||
|
}
|
||||||
|
edge.animated = false
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[rjb] Failed to update edge:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 继续执行下一个节点
|
||||||
|
executeNodeAnimation(nodeIndex + 1)
|
||||||
|
}, 1500) // 每个节点执行1.5秒
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始执行第一个节点
|
||||||
|
executeNodeAnimation(0)
|
||||||
|
}
|
||||||
|
|
||||||
// 监听节点和边的变化,检查是否有变更
|
// 监听节点和边的变化,检查是否有变更
|
||||||
watch([nodes, edges], () => {
|
watch([nodes, edges], () => {
|
||||||
if (lastSavedData.value) {
|
if (lastSavedData.value) {
|
||||||
@@ -2475,6 +2722,17 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
|||||||
|
|
||||||
const { current_node, executed_nodes, failed_nodes } = newStatus
|
const { current_node, executed_nodes, failed_nodes } = newStatus
|
||||||
console.log('[rjb] Execution status - current:', current_node, 'executed:', executed_nodes, 'failed:', failed_nodes)
|
console.log('[rjb] Execution status - current:', current_node, 'executed:', executed_nodes, 'failed:', failed_nodes)
|
||||||
|
console.log('[rjb] Execution status full:', JSON.stringify(newStatus, null, 2))
|
||||||
|
|
||||||
|
// 清除所有边的执行状态
|
||||||
|
edges.value.forEach(edge => {
|
||||||
|
if (edge.data) {
|
||||||
|
delete edge.data.executionStatus
|
||||||
|
}
|
||||||
|
if (edge.class) {
|
||||||
|
edge.class = edge.class.replace(/\b(edge-executing|edge-executed)\b/g, '').trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 更新正在执行的节点
|
// 更新正在执行的节点
|
||||||
if (current_node && current_node.node_id) {
|
if (current_node && current_node.node_id) {
|
||||||
@@ -2486,7 +2744,49 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
|||||||
class: nodeClass,
|
class: nodeClass,
|
||||||
data: nodeData
|
data: nodeData
|
||||||
})
|
})
|
||||||
console.log('[rjb] ✅ Marking node as executing:', current_node.node_id, 'class:', nodeClass)
|
console.log('[rjb] ✅ Marking node as executing:', current_node.node_id, 'class:', nodeClass, 'nodeData:', nodeData)
|
||||||
|
|
||||||
|
// 高亮连接到正在执行节点的边
|
||||||
|
edges.value.forEach(edge => {
|
||||||
|
if (edge.target === current_node.node_id) {
|
||||||
|
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executing').trim()
|
||||||
|
try {
|
||||||
|
if (updateEdge) {
|
||||||
|
updateEdge(edge.id, {
|
||||||
|
class: edgeClass,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#409eff',
|
||||||
|
strokeWidth: 3.5,
|
||||||
|
strokeDasharray: '8,4'
|
||||||
|
},
|
||||||
|
animated: true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果updateEdge不存在,直接修改(兼容旧版本)
|
||||||
|
edge.class = edgeClass
|
||||||
|
edge.style = {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#409eff',
|
||||||
|
strokeWidth: 3.5,
|
||||||
|
strokeDasharray: '8,4'
|
||||||
|
}
|
||||||
|
edge.animated = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[rjb] Failed to update edge:', e)
|
||||||
|
// 直接修改作为后备方案
|
||||||
|
edge.class = edgeClass
|
||||||
|
edge.style = {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#409eff',
|
||||||
|
strokeWidth: 3.5,
|
||||||
|
strokeDasharray: '8,4'
|
||||||
|
}
|
||||||
|
edge.animated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.warn('[rjb] ❌ Node not found:', current_node.node_id, 'available nodes:', nodes.value.map(n => n.id))
|
console.warn('[rjb] ❌ Node not found:', current_node.node_id, 'available nodes:', nodes.value.map(n => n.id))
|
||||||
}
|
}
|
||||||
@@ -2505,6 +2805,36 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
|||||||
data: nodeData
|
data: nodeData
|
||||||
})
|
})
|
||||||
console.log('[rjb] ✅ Marking node as executed:', executedNode.node_id, 'class:', nodeClass)
|
console.log('[rjb] ✅ Marking node as executed:', executedNode.node_id, 'class:', nodeClass)
|
||||||
|
|
||||||
|
// 高亮连接到已执行节点的边
|
||||||
|
edges.value.forEach(edge => {
|
||||||
|
if (edge.target === executedNode.node_id) {
|
||||||
|
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executed').trim()
|
||||||
|
try {
|
||||||
|
updateEdge(edge.id, {
|
||||||
|
class: edgeClass,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#67c23a',
|
||||||
|
strokeWidth: 3,
|
||||||
|
strokeDasharray: '0'
|
||||||
|
},
|
||||||
|
animated: false
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[rjb] Failed to update edge:', e)
|
||||||
|
// 如果updateEdge不存在,直接修改(兼容旧版本)
|
||||||
|
edge.class = edgeClass
|
||||||
|
edge.style = {
|
||||||
|
...edge.style,
|
||||||
|
stroke: '#67c23a',
|
||||||
|
strokeWidth: 3,
|
||||||
|
strokeDasharray: '0'
|
||||||
|
}
|
||||||
|
edge.animated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.warn('[rjb] ❌ Executed node not found:', executedNode.node_id)
|
console.warn('[rjb] ❌ Executed node not found:', executedNode.node_id)
|
||||||
}
|
}
|
||||||
@@ -2719,6 +3049,12 @@ onUnmounted(() => {
|
|||||||
// 禁用自动保存
|
// 禁用自动保存
|
||||||
disableAutoSave()
|
disableAutoSave()
|
||||||
|
|
||||||
|
// 清理测试动画定时器
|
||||||
|
if (testAnimationTimer.value) {
|
||||||
|
clearTimeout(testAnimationTimer.value)
|
||||||
|
testAnimationTimer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// 断开协作连接
|
// 断开协作连接
|
||||||
if (collaboration) {
|
if (collaboration) {
|
||||||
collaboration.disconnect()
|
collaboration.disconnect()
|
||||||
@@ -2987,19 +3323,26 @@ onUnmounted(() => {
|
|||||||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8) !important;
|
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8) !important;
|
||||||
animation: pulse-blue 1.5s infinite !important;
|
animation: pulse-blue 1.5s infinite !important;
|
||||||
transform: scale(1.05) !important;
|
transform: scale(1.05) !important;
|
||||||
z-index: 10 !important;
|
z-index: 3000 !important; /* Element Plus 模态框 z-index 通常是 2000-2100,我们设置更高 */
|
||||||
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-node.executed {
|
.custom-node.executed,
|
||||||
|
.vue-flow__node.executed .custom-node,
|
||||||
|
.vue-flow__node .custom-node.executed {
|
||||||
border: 3px solid #67c23a !important;
|
border: 3px solid #67c23a !important;
|
||||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3) !important;
|
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3) !important;
|
||||||
z-index: 5 !important;
|
z-index: 2999 !important; /* 比执行中的节点稍低,但仍在模态框之上 */
|
||||||
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-node.failed {
|
.custom-node.failed,
|
||||||
|
.vue-flow__node.failed .custom-node,
|
||||||
|
.vue-flow__node .custom-node.failed {
|
||||||
border: 3px solid #f56c6c !important;
|
border: 3px solid #f56c6c !important;
|
||||||
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3) !important;
|
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3) !important;
|
||||||
z-index: 5 !important;
|
z-index: 2999 !important; /* 比执行中的节点稍低,但仍在模态框之上 */
|
||||||
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 确保 Vue Flow 节点容器也能应用样式 */
|
/* 确保 Vue Flow 节点容器也能应用样式 */
|
||||||
@@ -3013,18 +3356,24 @@ onUnmounted(() => {
|
|||||||
.vue-flow__node .custom-node.executing {
|
.vue-flow__node .custom-node.executing {
|
||||||
border-color: #409eff !important;
|
border-color: #409eff !important;
|
||||||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8) !important;
|
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8) !important;
|
||||||
animation: pulse-blue 1.5s infinite !important;
|
animation: pulse-blue 1.5s infinite, node-pulse 2s ease-in-out infinite !important;
|
||||||
transform: scale(1.05) !important;
|
transform: scale(1.05) !important;
|
||||||
|
z-index: 3000 !important;
|
||||||
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-flow__node .custom-node.executed {
|
.vue-flow__node .custom-node.executed {
|
||||||
border-color: #67c23a !important;
|
border-color: #67c23a !important;
|
||||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3) !important;
|
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3) !important;
|
||||||
|
z-index: 2999 !important;
|
||||||
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vue-flow__node .custom-node.failed {
|
.vue-flow__node .custom-node.failed {
|
||||||
border-color: #f56c6c !important;
|
border-color: #f56c6c !important;
|
||||||
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3) !important;
|
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3) !important;
|
||||||
|
z-index: 2999 !important;
|
||||||
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-blue {
|
@keyframes pulse-blue {
|
||||||
@@ -3039,6 +3388,86 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 旋转动画 - 用于加载指示器 */
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强执行状态的动画效果 */
|
||||||
|
.custom-node.executing {
|
||||||
|
animation: pulse-blue 1.5s infinite, node-pulse 2s ease-in-out infinite !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes node-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 执行成功时的闪烁效果 */
|
||||||
|
.custom-node.executed {
|
||||||
|
animation: success-flash 0.5s ease-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes success-flash {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.5), 0 0 20px rgba(103, 194, 58, 0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(103, 194, 58, 0.3), 0 0 30px rgba(103, 194, 58, 1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 执行失败时的闪烁效果 */
|
||||||
|
.custom-node.failed {
|
||||||
|
animation: error-shake 0.5s ease-out !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes error-shake {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
10%, 30%, 50%, 70%, 90% {
|
||||||
|
transform: translateX(-3px);
|
||||||
|
}
|
||||||
|
20%, 40%, 60%, 80% {
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 边的执行状态样式 */
|
||||||
|
.vue-flow__edge.edge-executing .vue-flow__edge-path {
|
||||||
|
stroke: #409eff !important;
|
||||||
|
stroke-width: 3.5 !important;
|
||||||
|
stroke-dasharray: 8,4 !important;
|
||||||
|
animation: edge-flow 1.5s linear infinite !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__edge.edge-executed .vue-flow__edge-path {
|
||||||
|
stroke: #67c23a !important;
|
||||||
|
stroke-width: 3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes edge-flow {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 全局样式 - 优化边的选中效果(类似 Dify) */
|
/* 全局样式 - 优化边的选中效果(类似 Dify) */
|
||||||
.vue-flow__edge.selected .vue-flow__edge-path {
|
.vue-flow__edge.selected .vue-flow__edge-path {
|
||||||
stroke: #67c23a !important;
|
stroke: #67c23a !important;
|
||||||
|
|||||||
@@ -148,6 +148,19 @@ export const useAgentStore = defineStore('agent', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制Agent
|
||||||
|
const duplicateAgent = async (id: string, name?: string) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const requestData = name ? { name } : {}
|
||||||
|
const response = await api.post(`/api/v1/agents/${id}/duplicate`, requestData)
|
||||||
|
agents.value.unshift(response.data) // 添加到列表开头
|
||||||
|
return response.data
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置当前Agent
|
// 设置当前Agent
|
||||||
const setCurrentAgent = (agent: Agent | null) => {
|
const setCurrentAgent = (agent: Agent | null) => {
|
||||||
currentAgent.value = agent
|
currentAgent.value = agent
|
||||||
@@ -164,6 +177,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|||||||
deleteAgent,
|
deleteAgent,
|
||||||
deployAgent,
|
deployAgent,
|
||||||
stopAgent,
|
stopAgent,
|
||||||
|
duplicateAgent,
|
||||||
setCurrentAgent
|
setCurrentAgent
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
{{ formatDate(row.created_at) }}
|
{{ formatDate(row.created_at) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="300" fixed="right">
|
<el-table-column label="操作" width="350" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="handleEdit(row)">
|
<el-button link type="primary" @click="handleEdit(row)">
|
||||||
<el-icon><Edit /></el-icon>
|
<el-icon><Edit /></el-icon>
|
||||||
@@ -80,6 +80,10 @@
|
|||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
设计
|
设计
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button link type="info" @click="handleDuplicate(row)">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.status === 'draft' || row.status === 'stopped'"
|
v-if="row.status === 'draft' || row.status === 'stopped'"
|
||||||
link
|
link
|
||||||
@@ -177,7 +181,8 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Setting,
|
Setting,
|
||||||
VideoPlay,
|
VideoPlay,
|
||||||
VideoPause
|
VideoPause,
|
||||||
|
CopyDocument
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { useAgentStore } from '@/stores/agent'
|
import { useAgentStore } from '@/stores/agent'
|
||||||
import type { Agent } from '@/stores/agent'
|
import type { Agent } from '@/stores/agent'
|
||||||
@@ -374,6 +379,34 @@ const handleStop = async (agent: Agent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制
|
||||||
|
const handleDuplicate = async (agent: Agent) => {
|
||||||
|
try {
|
||||||
|
const { value } = await ElMessageBox.prompt(
|
||||||
|
`请输入新Agent的名称(留空将自动生成)`,
|
||||||
|
'复制Agent',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputPlaceholder: `留空将使用: ${agent.name} (副本)`,
|
||||||
|
inputValidator: (val: string) => {
|
||||||
|
if (val && val.length > 100) {
|
||||||
|
return '名称长度不能超过100个字符'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await agentStore.duplicateAgent(agent.id, value || undefined)
|
||||||
|
ElMessage.success('复制成功')
|
||||||
|
await loadAgents()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(error.response?.data?.detail || '复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
const handleDelete = async (agent: Agent) => {
|
const handleDelete = async (agent: Agent) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
:workflow-id="undefined"
|
:workflow-id="undefined"
|
||||||
:initial-nodes="initialNodes"
|
:initial-nodes="initialNodes"
|
||||||
:initial-edges="initialEdges"
|
:initial-edges="initialEdges"
|
||||||
|
:execution-status="executionStatus"
|
||||||
@save="handleSaveWorkflow"
|
@save="handleSaveWorkflow"
|
||||||
@node-test="handleNodeTest"
|
@node-test="handleNodeTest"
|
||||||
/>
|
/>
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
:opening-message="openingMessage"
|
:opening-message="openingMessage"
|
||||||
:preset-questions="presetQuestions"
|
:preset-questions="presetQuestions"
|
||||||
:node-test-result="nodeTestResult"
|
:node-test-result="nodeTestResult"
|
||||||
|
@execution-status="handleChatExecutionStatus"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,6 +202,12 @@ const loadWorkflowData = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理聊天界面的执行状态更新
|
||||||
|
const handleChatExecutionStatus = (status: any) => {
|
||||||
|
console.log('[rjb] WorkflowDesigner received execution status from chat:', JSON.stringify(status, null, 2))
|
||||||
|
executionStatus.value = status
|
||||||
|
}
|
||||||
|
|
||||||
// 测试Agent
|
// 测试Agent
|
||||||
const handleTestAgent = () => {
|
const handleTestAgent = () => {
|
||||||
testInput.value = '{}'
|
testInput.value = '{}'
|
||||||
@@ -214,8 +222,8 @@ const handleRunTest = async () => {
|
|||||||
testing.value = true
|
testing.value = true
|
||||||
testResult.value = null
|
testResult.value = null
|
||||||
|
|
||||||
// 轮询超时时间(60秒)
|
// 轮询超时时间(5分钟,复杂工作流可能需要更长时间)
|
||||||
const maxPollingTime = 60000
|
const maxPollingTime = 5 * 60 * 1000 // 5分钟 = 300000毫秒
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
let pollingInterval: any = null
|
let pollingInterval: any = null
|
||||||
|
|
||||||
|
|||||||
270
智能需求分析与解决方案生成器_使用说明.md
Normal file
270
智能需求分析与解决方案生成器_使用说明.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# 智能需求分析与解决方案生成器 - 使用说明
|
||||||
|
|
||||||
|
## 📋 Agent 基本信息
|
||||||
|
|
||||||
|
- **名称**: 智能需求分析与解决方案生成器
|
||||||
|
- **状态**: 已发布(可直接使用)
|
||||||
|
- **节点数量**: 8个节点
|
||||||
|
- **连接数量**: 8条连接
|
||||||
|
- **Agent ID**: `9195f8df-3340-46a0-b761-b955a57acc6c`
|
||||||
|
|
||||||
|
## 🎯 功能概述
|
||||||
|
|
||||||
|
这是一个智能的多节点Agent,能够:
|
||||||
|
1. **自动理解用户需求** - 分析用户输入,提取关键信息
|
||||||
|
2. **智能需求分类** - 根据需求类型自动分类(技术方案/业务流程/数据分析等)
|
||||||
|
3. **专业方案生成** - 调用专业模板或LLM生成针对性解决方案
|
||||||
|
4. **方案整合优化** - 整合各分支结果,优化输出格式
|
||||||
|
5. **专业文档输出** - 生成结构化的Markdown文档
|
||||||
|
|
||||||
|
## 🔄 工作流结构
|
||||||
|
|
||||||
|
```
|
||||||
|
开始节点
|
||||||
|
↓
|
||||||
|
需求理解与分析节点(LLM)
|
||||||
|
↓
|
||||||
|
需求分类节点(条件判断)
|
||||||
|
├─→ [技术方案分支] → 技术方案设计节点(Template/LLM)
|
||||||
|
└─→ [业务流程分支] → 业务流程设计节点(Template/LLM)
|
||||||
|
↓ ↓
|
||||||
|
└────→ 方案整合节点(Transform)
|
||||||
|
↓
|
||||||
|
输出优化与格式化节点(LLM)
|
||||||
|
↓
|
||||||
|
结束节点
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 节点详细说明
|
||||||
|
|
||||||
|
### 1. 开始节点(start-1)
|
||||||
|
- **功能**: 接收用户输入
|
||||||
|
- **输入格式**: JSON格式
|
||||||
|
- **输出**: 将用户输入传递给需求理解节点
|
||||||
|
|
||||||
|
### 2. 需求理解与分析节点(llm-requirement-analysis)
|
||||||
|
- **类型**: LLM节点
|
||||||
|
- **模型**: DeepSeek Chat
|
||||||
|
- **功能**:
|
||||||
|
- 分析用户需求
|
||||||
|
- 提取关键信息
|
||||||
|
- 识别需求类型、复杂度、领域等
|
||||||
|
- **输出格式**: JSON
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requirement_type": "技术方案|业务流程|数据分析|工作流设计|其他",
|
||||||
|
"key_points": ["关键点1", "关键点2", ...],
|
||||||
|
"complexity": "简单|中等|复杂",
|
||||||
|
"domain": "领域(如:电商、金融、教育等)",
|
||||||
|
"summary": "需求摘要"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 需求分类节点(condition-classify)
|
||||||
|
- **类型**: 条件节点
|
||||||
|
- **功能**: 根据需求类型进行分支
|
||||||
|
- **判断条件**:
|
||||||
|
- 如果需求类型是"技术方案"或"API集成" → 走技术方案分支
|
||||||
|
- 否则 → 走业务流程分支
|
||||||
|
|
||||||
|
### 4. 技术方案设计节点
|
||||||
|
- **类型**: Template节点(优先)或LLM节点(备用)
|
||||||
|
- **功能**: 生成技术方案
|
||||||
|
- **输出内容**:
|
||||||
|
- 技术选型建议
|
||||||
|
- 架构设计
|
||||||
|
- 实施步骤
|
||||||
|
- 风险评估
|
||||||
|
- 最佳实践建议
|
||||||
|
|
||||||
|
### 5. 业务流程设计节点
|
||||||
|
- **类型**: Template节点(优先)或LLM节点(备用)
|
||||||
|
- **功能**: 生成业务流程方案
|
||||||
|
- **输出内容**:
|
||||||
|
- 流程概述
|
||||||
|
- 关键步骤
|
||||||
|
- 角色与职责
|
||||||
|
- 流程优化建议
|
||||||
|
- 实施路线图
|
||||||
|
|
||||||
|
### 6. 方案整合节点(transform-integration)
|
||||||
|
- **类型**: Transform节点
|
||||||
|
- **功能**: 整合各分支的结果
|
||||||
|
- **模式**: 合并模式
|
||||||
|
- **输出**: 包含解决方案、需求分析、时间戳的整合数据
|
||||||
|
|
||||||
|
### 7. 输出优化与格式化节点(llm-optimization)
|
||||||
|
- **类型**: LLM节点
|
||||||
|
- **模型**: DeepSeek Chat
|
||||||
|
- **功能**:
|
||||||
|
- 优化文档结构
|
||||||
|
- 补充关键细节
|
||||||
|
- 添加实施建议
|
||||||
|
- 统一格式
|
||||||
|
- **输出**: 完整的Markdown文档
|
||||||
|
|
||||||
|
### 8. 结束节点(end-1)
|
||||||
|
- **功能**: 返回最终结果
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 方法一:通过Agent管理界面测试
|
||||||
|
|
||||||
|
1. **进入Agent管理页面**
|
||||||
|
- 在左侧菜单选择"Agent管理"
|
||||||
|
- 找到"智能需求分析与解决方案生成器"
|
||||||
|
|
||||||
|
2. **测试Agent**
|
||||||
|
- 点击Agent名称进入详情页
|
||||||
|
- 点击"测试"按钮
|
||||||
|
- 在输入框中输入你的需求
|
||||||
|
|
||||||
|
3. **输入示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "我需要设计一个电商系统的用户订单处理流程,包括下单、支付、发货、售后等环节"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
或者简单输入:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "设计一个微服务架构的在线教育平台"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **查看结果**
|
||||||
|
- 等待Agent执行完成(通常需要30-60秒)
|
||||||
|
- 查看生成的解决方案文档
|
||||||
|
|
||||||
|
### 方法二:通过API调用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/v1/executions
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <your_token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"agent_id": "9195f8df-3340-46a0-b761-b955a57acc6c",
|
||||||
|
"input_data": {
|
||||||
|
"query": "你的需求描述"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 使用场景示例
|
||||||
|
|
||||||
|
### 场景1:技术方案设计
|
||||||
|
**输入**:
|
||||||
|
```
|
||||||
|
设计一个高并发的实时推荐系统,需要支持千万级用户,毫秒级响应
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出**:
|
||||||
|
- 技术选型(Redis、Kafka、Spark等)
|
||||||
|
- 架构设计图
|
||||||
|
- 实施步骤
|
||||||
|
- 性能优化建议
|
||||||
|
|
||||||
|
### 场景2:业务流程优化
|
||||||
|
**输入**:
|
||||||
|
```
|
||||||
|
优化我们公司的客户服务流程,提高客户满意度
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出**:
|
||||||
|
- 当前流程分析
|
||||||
|
- 优化建议
|
||||||
|
- 新流程设计
|
||||||
|
- 实施路线图
|
||||||
|
|
||||||
|
### 场景3:系统架构设计
|
||||||
|
**输入**:
|
||||||
|
```
|
||||||
|
设计一个微服务架构的电商平台,包括用户、商品、订单、支付等模块
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出**:
|
||||||
|
- 微服务拆分方案
|
||||||
|
- 服务间通信设计
|
||||||
|
- 数据一致性方案
|
||||||
|
- 部署架构
|
||||||
|
|
||||||
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
|
### 节点配置参数
|
||||||
|
|
||||||
|
- **LLM节点温度**: 0.5-0.7(平衡创造性和准确性)
|
||||||
|
- **最大Token数**: 2000-3000(确保输出完整)
|
||||||
|
- **模型**: DeepSeek Chat(默认)
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
|
||||||
|
如果需要修改Agent配置:
|
||||||
|
1. 进入Agent管理页面
|
||||||
|
2. 点击"设计"按钮
|
||||||
|
3. 在可视化编辑器中修改节点配置
|
||||||
|
4. 保存并发布
|
||||||
|
|
||||||
|
## 📝 输出格式
|
||||||
|
|
||||||
|
Agent最终输出为结构化的Markdown文档,包含:
|
||||||
|
|
||||||
|
1. **需求摘要**
|
||||||
|
2. **需求分析结果**
|
||||||
|
- 需求类型
|
||||||
|
- 关键点
|
||||||
|
- 复杂度评估
|
||||||
|
- 领域识别
|
||||||
|
3. **解决方案**
|
||||||
|
- 详细方案内容
|
||||||
|
- 实施建议
|
||||||
|
- 最佳实践
|
||||||
|
4. **附录**
|
||||||
|
- 相关资源
|
||||||
|
- 注意事项
|
||||||
|
|
||||||
|
## 🔍 注意事项
|
||||||
|
|
||||||
|
1. **输入格式**: 建议使用JSON格式,包含`query`字段
|
||||||
|
2. **执行时间**: 复杂需求可能需要60-120秒
|
||||||
|
3. **Token限制**: 如果输出被截断,可以增加`max_tokens`参数
|
||||||
|
4. **需求描述**: 越详细的需求描述,生成的方案越准确
|
||||||
|
|
||||||
|
## 🛠️ 故障排查
|
||||||
|
|
||||||
|
### 问题1:Agent执行超时
|
||||||
|
**解决方案**:
|
||||||
|
- 检查网络连接
|
||||||
|
- 确认LLM服务正常
|
||||||
|
- 简化需求描述
|
||||||
|
|
||||||
|
### 问题2:输出不完整
|
||||||
|
**解决方案**:
|
||||||
|
- 增加`max_tokens`参数
|
||||||
|
- 分段处理复杂需求
|
||||||
|
|
||||||
|
### 问题3:分类不准确
|
||||||
|
**解决方案**:
|
||||||
|
- 在需求描述中明确说明需求类型
|
||||||
|
- 例如:"我需要一个技术方案..."或"请帮我设计业务流程..."
|
||||||
|
|
||||||
|
## 📈 性能优化建议
|
||||||
|
|
||||||
|
1. **缓存机制**: 对于相似需求,可以复用之前的分析结果
|
||||||
|
2. **并行处理**: 对于多个独立需求,可以并行执行
|
||||||
|
3. **模板优化**: 根据使用频率优化模板选择逻辑
|
||||||
|
|
||||||
|
## 🔄 更新日志
|
||||||
|
|
||||||
|
- **v1.0** (2026-01-19): 初始版本
|
||||||
|
- 支持需求理解和分类
|
||||||
|
- 支持技术方案和业务流程生成
|
||||||
|
- 支持输出优化和格式化
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
如有问题或建议,请联系系统管理员或查看系统日志。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2026-01-19
|
||||||
Reference in New Issue
Block a user