Merge branch 'mcp' into mcp_debug

# Conflicts:
#	dist/copilot_web/App-C2CSnZyL.js
#	dist/copilot_web/App-DT2fOvHP.js
#	dist/copilot_web/App-DfYXBz6y.js
#	dist/copilot_web/DebugGuide-CqUJVeP7.js
#	dist/copilot_web/DebugGuide-nVo3jJ10.js
#	dist/copilot_web/DebugGuide-oR460wi1.js
#	dist/copilot_web/DebugResult-swqmG-gW.js
#	dist/copilot_web/WorkflowOption-BRKJ_AoR.js
#	dist/copilot_web/WorkflowOption-BXMoNmz0.js
#	dist/copilot_web/WorkflowOption-KaEJtkSh.js
#	dist/copilot_web/input.js
#	dist/copilot_web/message-components-DEIvJ4jZ.js
#	dist/copilot_web/workflowChat-CgW0f6JP.js
This commit is contained in:
li
2025-08-08 17:11:20 +08:00
28 changed files with 1918 additions and 1085 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ set_tracing_disabled(True)
def create_agent(**kwargs) -> Agent:
# 通过用户配置拿/环境变量
client = AsyncOpenAI(
api_key="aib_ComfyUI_Copilot_8d28a3",
api_key="",
base_url="https://comfyui-copilot-server-pre.onrender.com/v1",
)
+83 -26
View File
@@ -19,7 +19,7 @@ import sys
import os
from ..service.debug_agent import debug_workflow_errors
from ..service.database import save_workflow_data, get_workflow_data_by_id
from ..service.database import save_workflow_data, get_workflow_data_by_id, update_workflow_ui_by_id
from ..service.mcp_client import comfyui_agent_invoke, ImageData
@@ -330,7 +330,7 @@ async def invoke_chat(request):
intent = req_json.get('intent')
ext = req_json.get('ext')
historical_messages = req_json.get('messages', [])
workflow_data = req_json.get('workflow_data')
workflow_checkpoint_id = req_json.get('workflow_checkpoint_id')
# 获取当前语言
language = request.headers.get('Accept-Language', 'en')
@@ -345,24 +345,11 @@ async def invoke_chat(request):
# 历史消息已经从前端传递过来,格式为OpenAI格式,直接使用
print(f"-- Received {len(historical_messages)} historical messages")
# Save current workflow data to database if provided
if workflow_data and session_id:
try:
print(f"Saving workflow data for session {session_id}")
save_workflow_data(
session_id=session_id,
workflow_data=workflow_data,
workflow_data_ui=None, # UI format not available from frontend
attributes={
"source": "frontend_current_workflow",
"description": "Current workflow data from canvas",
"timestamp": time.time()
}
)
print(f"Successfully saved workflow data for session {session_id}")
except Exception as e:
print(f"Failed to save workflow data for session {session_id}: {str(e)}")
# Continue without failing the request
# Log workflow checkpoint ID if provided (workflow is now pre-saved before invoke)
if workflow_checkpoint_id:
print(f"Using workflow checkpoint ID: {workflow_checkpoint_id} for session {session_id}")
else:
print(f"No workflow checkpoint ID provided for session {session_id}")
# 历史消息已经从前端传递过来,包含了正确格式的OpenAI消息(包括图片)
# 直接使用前端传递的历史消息,无需重新构建当前消息
@@ -381,6 +368,7 @@ async def invoke_chat(request):
config = {
"session_id": session_id, # 添加session_id到配置中
"workflow_checkpoint_id": workflow_checkpoint_id, # 添加工作流检查点ID
"openai_api_key": request.headers.get('Openai-Api-Key'),
"openai_base_url": request.headers.get('Openai-Base-Url'),
"model_select": next((x['data'][0] for x in ext if x['type'] == 'model_select' and x.get('data')), None)
@@ -468,7 +456,8 @@ async def save_workflow_checkpoint(request):
session_id = req_json.get('session_id')
workflow_api = req_json.get('workflow_api') # API format workflow
workflow_ui = req_json.get('workflow_ui') # UI format workflow
checkpoint_type = req_json.get('checkpoint_type', 'debug_start') # debug_start or debug_complete
checkpoint_type = req_json.get('checkpoint_type', 'debug_start') # debug_start, debug_complete, user_message_checkpoint
message_id = req_json.get('message_id') # User message ID for linking (optional)
if not session_id or not workflow_api:
return web.json_response({
@@ -479,10 +468,19 @@ async def save_workflow_checkpoint(request):
# Save workflow with checkpoint type in attributes
attributes = {
"checkpoint_type": checkpoint_type,
"description": f"Workflow checkpoint: {checkpoint_type}",
"timestamp": time.time()
}
# Set description and additional attributes based on checkpoint type
if checkpoint_type == "user_message_checkpoint" and message_id:
attributes.update({
"description": f"Workflow checkpoint before user message {message_id}",
"message_id": message_id,
"source": "user_message_pre_invoke"
})
else:
attributes["description"] = f"Workflow checkpoint: {checkpoint_type}"
version_id = save_workflow_data(
session_id=session_id,
workflow_data=workflow_api,
@@ -492,12 +490,21 @@ async def save_workflow_checkpoint(request):
print(f"Workflow checkpoint saved with version ID: {version_id}")
# Return response format based on checkpoint type
response_data = {
"version_id": version_id,
"checkpoint_type": checkpoint_type
}
if checkpoint_type == "user_message_checkpoint" and message_id:
response_data.update({
"checkpoint_id": version_id, # Add checkpoint_id alias for user message checkpoints
"message_id": message_id
})
return web.json_response({
"success": True,
"data": {
"version_id": version_id,
"checkpoint_type": checkpoint_type
},
"data": response_data,
"message": f"Workflow checkpoint saved successfully"
})
@@ -509,6 +516,7 @@ async def save_workflow_checkpoint(request):
})
@server.PromptServer.instance.routes.get("/api/restore-workflow-checkpoint")
async def restore_workflow_checkpoint(request):
"""
@@ -746,3 +754,52 @@ async def invoke_debug(request):
await response.write_eof()
return response
@server.PromptServer.instance.routes.post("/api/update-workflow-ui")
async def update_workflow_ui(request):
"""
Update workflow_data_ui field for a specific checkpoint without affecting other fields
"""
print("Received update-workflow-ui request")
req_json = await request.json()
try:
checkpoint_id = req_json.get('checkpoint_id')
workflow_data_ui = req_json.get('workflow_data_ui')
if not checkpoint_id or not workflow_data_ui:
return web.json_response({
"success": False,
"message": "Missing required parameters: checkpoint_id and workflow_data_ui"
})
try:
checkpoint_id = int(checkpoint_id)
except ValueError:
return web.json_response({
"success": False,
"message": "Invalid checkpoint_id format"
})
# Update only the workflow_data_ui field
success = update_workflow_ui_by_id(checkpoint_id, workflow_data_ui)
if success:
print(f"Successfully updated workflow_data_ui for checkpoint ID: {checkpoint_id}")
return web.json_response({
"success": True,
"message": f"Workflow UI data updated successfully for checkpoint {checkpoint_id}"
})
else:
return web.json_response({
"success": False,
"message": f"Checkpoint {checkpoint_id} not found"
})
except Exception as e:
print(f"Error updating workflow UI data: {str(e)}")
return web.json_response({
"success": False,
"message": f"Failed to update workflow UI data: {str(e)}"
})
+37
View File
@@ -0,0 +1,37 @@
[
{
"name": "添加LoRA",
"description": "在现有工作流中添加LoRA节点",
"content": "在现有工作流中添加LoRA节点,确保与现有模型和提示词节点正确连接\n 在checkpoint节点后添加LoRA节点。\n {\n \"1\": {\n \"inputs\": {\n \"lora_name\": \"DOG.safetensors\",\n \"strength_model\": 1,\n \"strength_clip\": 1,\n \"model\": [\n \"2\",\n 0\n ],\n \"clip\": [\n \"2\",\n 1\n ]\n },\n \"class_type\": \"LoraLoader\",\n \"_meta\": {\n \"title\": \"Load LoRA\"\n }\n }"
},
{
"name": "后处理增强",
"description": "后处理增强,例如在Preview Image或Save Image节点后添加高清放大功能(如Real-ESRGAN、ESRGAN等); 或者添加图像缩放节点",
"content": "- 在Preview Image或Save Image节点后添加高清放大功能(如Real-ESRGAN、ESRGAN等)\n - 添加图像缩放节点\n {\n \"1\": {\n \"inputs\": {\n \"width\": 512,\n \"height\": 512,\n \"interpolation\": \"nearest\",\n \"method\": \"stretch\",\n \"condition\": \"always\",\n \"multiple_of\": 0\n },\n \"class_type\": \"ImageResize+\",\n \"_meta\": {\n \"title\": \"🔧 Image Resize\"\n }\n }\n}\n - 添加图像尺寸调整节点\n {\n \"2\": {\n \"inputs\": {\n \"aspect_ratio\": \"original\",\n \"proportional_width\": 2,\n \"proportional_height\": 1,\n \"fit\": \"letterbox\",\n \"method\": \"lanczos\",\n \"round_to_multiple\": \"8\",\n \"scale_to_longest_side\": false,\n \"longest_side\": 1024\n },\n \"class_type\": \"LayerUtility: ImageScaleByAspectRatio\",\n \"_meta\": {\n \"title\": \"LayerUtility: ImageScaleByAspectRatio\"\n }\n }\n}\n -添加图像放大节点\n \"11\": {\n \"inputs\": {\n \"width\": 512,\n \"height\": 512,\n \"upscale_method\": \"nearest-exact\",\n \"keep_proportion\": false,\n \"divisible_by\": 2,\n \"crop\": \"disabled\",\n \"image\": [\n \"12\",\n 0\n ]\n },\n \"class_type\": \"ImageResizeKJ\",\n \"_meta\": {\n \"title\": \"Resize Image\"\n }\n },\n -添加VAEdecode节点\n \"12\": {\n \"inputs\": {},\n \"class_type\": \"VAEDecode\",\n \"_meta\": {\n \"title\": \"VAE Decode\"\n }\n }\n}"
},
{
"name": "提示词优化",
"description": "修改现有提示词节点的内容(提示词应该在(CLIP Text Encode Prompt或Text _O节点内编辑); 添加单独的提示词输入节点",
"content": "{\n \"12\": {\n \"inputs\": {\n \"text\": [\n \"13\",\n 0\n ]\n },\n \"class_type\": \"CLIPTextEncode\",\n \"_meta\": {\n \"title\": \"CLIP Text Encode (Prompt)\"\n }\n },\n \"13\": {\n \"inputs\": {\n \"delimiter\": \", \",\n \"clean_whitespace\": \"true\",\n \"text_a\": [\n \"15\",\n 0\n ],\n \"text_b\": [\n \"16\",\n 0\n ]\n },\n \"class_type\": \"Text Concatenate\",\n \"_meta\": {\n \"title\": \"Text Concatenate\"\n }\n },\n \"15\": {\n \"inputs\": {\n \"text\": \"\"\n },\n \"class_type\": \"Text _O\",\n \"_meta\": {\n \"title\": \"Text _O\"\n }\n },\n \"16\": {\n \"inputs\": {\n \"text\": \"\"\n },\n \"class_type\": \"Text _O\",\n \"_meta\": {\n \"title\": \"Text _O\"\n }\n }\n}"
},
{
"name": "图像反推",
"description": "理解图片信息并用文字表达出来",
"content": "添加图像反推节点(如CLIP Interrogator)\n{\n \"11\": {\n \"inputs\": {\n \"prompt_mode\": \"fast\",\n \"image_analysis\": \"off\"\n },\n \"class_type\": \"ClipInterrogator\",\n \"_meta\": {\n \"title\": \"Clip Interrogator ♾️Mixlab\"\n }\n }\n}\n - 添加更复杂或者更好用的图像反推节点(加载图片使用florence2进行反推)\n {\n \"6\": {\n \"inputs\": {\n \"image\": \"06.JPG\"\n },\n \"class_type\": \"LoadImage\",\n \"_meta\": {\n \"title\": \"$image.image!:The image to analyze, must be a url\"\n }\n },\n \"10\": {\n \"inputs\": {\n \"model\": \"microsoft/Florence-2-large\",\n \"precision\": \"fp16\",\n \"attention\": \"sdpa\"\n },\n \"class_type\": \"DownloadAndLoadFlorence2Model\",\n \"_meta\": {\n \"title\": \"DownloadAndLoadFlorence2Model\"\n }\n },\n \"11\": {\n \"inputs\": {\n \"text_input\": \"\",\n \"task\": \"more_detailed_caption\",\n \"fill_mask\": true,\n \"keep_model_loaded\": false,\n \"max_new_tokens\": 1024,\n \"num_beams\": 3,\n \"do_sample\": true,\n \"output_mask_select\": \"\",\n \"seed\": 1098631327477633,\n \"image\": [\n \"6\",\n 0\n ],\n \"florence2_model\": [\n \"10\",\n 0\n ]\n },\n \"class_type\": \"Florence2Run\",\n \"_meta\": {\n \"title\": \"Florence2Run\"\n }\n },\n \"18\": {\n \"inputs\": {\n \"anything\": [\n \"11\",\n 2\n ]\n },\n \"class_type\": \"easy showAnything\",\n \"_meta\": {\n \"title\": \"Show Any\"\n }\n },\n \"20\": {\n \"inputs\": {\n \"value\": \"Generate high-quality text descriptions from images using local Florence model.\\n \\n Main use cases:\\n 1. **Reverse image prompt generation**:\\n - When users upload an image and want to get prompts for AI art generation\\n - Analyzes visual elements, style, composition, etc. to generate prompts for Stable Diffusion, DALL-E, and other models\\n - In this case, return the tool's raw output directly to users without any modification or summary\\n - Users can directly use these prompts for image generation\\n 2. **Image content understanding**:\\n - When users ask about specific content, objects, scenes, people, etc. in the image\\n - Need to understand the semantic content of the image and answer users' specific questions\\n - In this case, combine tool output with conversation context to give users contextually appropriate natural responses\\n - Don't return raw output directly, but provide targeted replies based on understanding results\"\n },\n \"class_type\": \"PrimitiveStringMultiline\",\n \"_meta\": {\n \"title\": \"MCP\"\n }\n }\n}"
},
{
"name": "添加ControlNet",
"description": "在使用风格迁移保持主体结构不变、保持主体形状、重新绘制背景、控制人物角色姿态、保持场景的深度和空间结构、基于线稿或者草图生成图像等场景下,可以用添加ControlNet节点来实现",
"content": "**ControlNet集成**:(preprocessor可选canny、depth等参数,模型样式选择,Controlnet开始、结束权重)\n {\n \"22\": {\n \"inputs\": {\n \"strength\": 1,\n \"start_percent\": 0,\n \"end_percent\": 1,\n \"control_net\": [\n \"23\",\n 0\n ],\n \"image\": [\n \"24\",\n 0\n ]\n },\n \"class_type\": \"ControlNetApplyAdvanced\",\n \"_meta\": {\n \"title\": \"Apply ControlNet\"\n }\n },\n \"23\": {\n \"inputs\": {\n \"control_net_name\": \"ControlNet-Standard-Lineart-for-SDXL.safetensors\"\n },\n \"class_type\": \"ControlNetLoader\",\n \"_meta\": {\n \"title\": \"Load ControlNet Model\"\n }\n },\n \"24\": {\n \"inputs\": {\n \"preprocessor\": \"none\",\n \"resolution\": 512\n },\n \"class_type\": \"AIO_Preprocessor\",\n \"_meta\": {\n \"title\": \"AIO Aux Preprocessor\"\n }\n }\n}"
},
{
"name": "扩图",
"description": "给我一个扩图链路(包括图像加载和图像输出)",
"content": "{\n \"1\": {\n \"inputs\": {\n \"image\": \"01.JPG\"\n },\n \"class_type\": \"LoadImage\",\n \"_meta\": {\n \"title\": \"$image.image!:The image to analyze, must be a url\"\n }\n },\n \"2\": {\n \"inputs\": {\n \"model\": \"runwayml/stable-diffusion-xl-base-1.0\",\n \"precision\": \"fp16\",\n \"attention\": \"sdpa\"\n },\n \"class_type\": \"DownloadAndLoadModel\",\n \"_meta\": {\n \"title\": \"DownloadAndLoadModel\"\n }\n },\n \"3\": {\n \"inputs\": {\n \"prompt\": \"A beautiful girl with long hair and a white dress\",\n \"image\": [\n \"1\",\n 0\n ],\n \"model\": [\n \"2\",\n 0\n ]\n },\n \"class_type\": \"StableDiffusionXLRun\",\n \"_meta\": {\n \"title\": \"StableDiffusionXLRun\"\n }\n },\n \"4\": {\n \"inputs\": {\n \"image\": [\n \"3\",\n 0\n ]\n },\n \"class_type\": \"easy showImage\",\n \"_meta\": {\n \"title\": \"Show Image\"\n }\n }\n}"
},
{
"name": "智能抠图",
"description": "添加背景移除节点(如SAM、U²-Net等)",
"content": "- 添加背景移除或抠图节点\n {\n \"7\": {\n \"inputs\": {\n \"rem_mode\": \"RMBG-1.4\",\n \"image_output\": \"Preview\",\n \"save_prefix\": \"ComfyUI\",\n \"torchscript_jit\": false,\n \"add_background\": \"none\",\n \"refine_foreground\": false\n },\n \"class_type\": \"easy imageRemBg\",\n \"_meta\": {\n \"title\": \"Image Remove Bg\"\n }\n }\n}\n - 添加SAM抠图节点\n {\n \"8\": {\n \"inputs\": {\n \"prompt\": \"\",\n \"threshold\": 0.3,\n \"sam_model\": [\n \"9\",\n 0\n ],\n \"grounding_dino_model\": [\n \"10\",\n 0\n ]\n },\n \"class_type\": \"GroundingDinoSAMSegment (segment anything)\",\n \"_meta\": {\n \"title\": \"GroundingDinoSAMSegment (segment anything)\"\n }\n },\n \"9\": {\n \"inputs\": {\n \"model_name\": \"sam_vit_h (2.56GB)\"\n },\n \"class_type\": \"SAMModelLoader (segment anything)\",\n \"_meta\": {\n \"title\": \"SAMModelLoader (segment anything)\"\n }\n },\n \"10\": {\n \"inputs\": {\n \"model_name\": \"GroundingDINO_SwinT_OGC (694MB)\"\n },\n \"class_type\": \"GroundingDinoModelLoader (segment anything)\",\n \"_meta\": {\n \"title\": \"GroundingDinoModelLoader (segment anything)\"\n }\n }\n}"
}
]
+43 -1
View File
@@ -86,6 +86,21 @@ class DatabaseManager:
finally:
session.close()
def get_current_workflow_data_ui(self, session_id: str) -> Optional[Dict[str, Any]]:
"""获取当前session的最新工作流数据(最大ID版本)"""
session = self.get_session()
try:
latest_version = session.query(WorkflowVersion)\
.filter(WorkflowVersion.session_id == session_id)\
.order_by(WorkflowVersion.id.desc())\
.first()
if latest_version:
return json.loads(latest_version.workflow_data_ui)
return None
finally:
session.close()
def get_workflow_version_by_id(self, version_id: int) -> Optional[Dict[str, Any]]:
"""根据版本ID获取工作流数据"""
session = self.get_session()
@@ -124,6 +139,25 @@ class DatabaseManager:
raise e
finally:
session.close()
def update_workflow_ui(self, version_id: int, workflow_data_ui: Dict[str, Any]) -> bool:
"""只更新指定版本的workflow_data_ui字段,不影响其他字段"""
session = self.get_session()
try:
version = session.query(WorkflowVersion)\
.filter(WorkflowVersion.id == version_id)\
.first()
if version:
version.workflow_data_ui = json.dumps(workflow_data_ui)
session.commit()
return True
return False
except Exception as e:
session.rollback()
raise e
finally:
session.close()
# 全局数据库管理器实例
db_manager = DatabaseManager()
@@ -132,10 +166,18 @@ def get_workflow_data(session_id: str) -> Optional[Dict[str, Any]]:
"""获取当前session的工作流数据的便捷函数"""
return db_manager.get_current_workflow_data(session_id)
def get_workflow_data_ui(session_id: str) -> Optional[Dict[str, Any]]:
"""获取当前session的工作流数据的便捷函数"""
return db_manager.get_current_workflow_data_ui(session_id)
def save_workflow_data(session_id: str, workflow_data: Dict[str, Any], workflow_data_ui: Dict[str, Any] = None, attributes: Optional[Dict[str, Any]] = None) -> int:
"""保存工作流数据的便捷函数"""
return db_manager.save_workflow_version(session_id, workflow_data, workflow_data_ui, attributes)
def get_workflow_data_by_id(version_id: int) -> Optional[Dict[str, Any]]:
"""根据版本ID获取工作流数据的便捷函数"""
return db_manager.get_workflow_version_by_id(version_id)
return db_manager.get_workflow_version_by_id(version_id)
def update_workflow_ui_by_id(version_id: int, workflow_data_ui: Dict[str, Any]) -> bool:
"""只更新指定版本的workflow_data_ui字段的便捷函数"""
return db_manager.update_workflow_ui(version_id, workflow_data_ui)
+90 -79
View File
@@ -54,11 +54,8 @@ async def run_workflow(session_id: str) -> str:
@function_tool
def analyze_error_type(error_data: str) -> str:
"""分析错误类型,判断应该使用哪个agent,输入应为JSON字符串"""
"""分析错误类型,判断应该使用哪个agent,输入可以是JSON字符串或普通文本"""
try:
# 解析JSON字符串
error_dict = json.loads(error_data) if isinstance(error_data, str) else error_data
error_analysis = {
"error_type": "unknown",
"recommended_agent": "workflow_bugfix_default_agent",
@@ -66,82 +63,95 @@ def analyze_error_type(error_data: str) -> str:
"affected_nodes": []
}
# 检查新的返回格式
if "success" in error_dict:
if error_dict["success"]:
# 工作流验证成功
error_analysis["error_type"] = "no_error"
error_analysis["recommended_agent"] = "none"
error_analysis["error_details"] = [{"message": "Workflow validation successful"}]
return json.dumps(error_analysis)
else:
# 工作流验证失败,继续分析错误类型
pass
# 将输入转换为字符串进行关键词匹配
error_text = str(error_data).lower()
# 检查是否有node_errors (ComfyUI验证错误格式)
if "node_errors" in error_dict and error_dict["node_errors"]:
node_errors = error_dict["node_errors"]
parameter_errors = 0
connection_errors = 0
other_errors = 0
for node_id, node_error in node_errors.items():
error_analysis["affected_nodes"].append(node_id)
if "errors" in node_error:
for error in node_error["errors"]:
error_detail = {
"node_id": node_id,
"error_type": error.get("type", "unknown"),
"message": error.get("message", ""),
"details": error.get("details", "")
}
error_analysis["error_details"].append(error_detail)
# 改进的错误类型判断
error_detail_str = json.dumps(error_detail).lower()
error_type = error.get("type", "").lower()
# error_message = error.get("message", "").lower()
# 优先判断连接相关错误(结构性错误)
if (any(keyword in error_detail_str for keyword in [
"connection", "input connection", "required input", "missing input",
"not connected", "no connection", "link", "output", "socket"
]) or
error_type in ["missing_input", "invalid_connection", "connection_error"]):
connection_errors += 1
# 参数相关错误的判断(更精确)
elif (error_type in ["value_not_in_list", "invalid_input", "invalid_value"] or
any(keyword in error_detail_str for keyword in [
"value not in list", "invalid value", "not found in list",
"parameter value", "invalid parameter", "model not found", "invalid image file"
])):
parameter_errors += 1
# 其他结构性错误
else:
other_errors += 1
# 根据错误类型决定使用哪个agent
if connection_errors > 0 and parameter_errors == 0 and other_errors == 0:
# 纯连接错误,使用专门的link_agent
error_analysis["error_type"] = "connection_error"
error_analysis["recommended_agent"] = "link_agent"
elif connection_errors > 0:
# 混合错误,优先处理连接问题
error_analysis["error_type"] = "mixed_connection_error"
error_analysis["recommended_agent"] = "link_agent"
elif parameter_errors > 0:
error_analysis["error_type"] = "parameter_error"
error_analysis["recommended_agent"] = "parameter_agent"
else:
error_analysis["error_type"] = "structural_error"
error_analysis["recommended_agent"] = "workflow_bugfix_default_agent"
# 检查成功状态
if any(keyword in error_text for keyword in [
'"success": true', "'success': true", "validation successful", "workflow validation successful"
]):
error_analysis["error_type"] = "no_error"
error_analysis["recommended_agent"] = "none"
error_analysis["error_details"] = [{"message": "Workflow validation successful"}]
return json.dumps(error_analysis)
# 统计不同类型的错误
parameter_errors = 0
connection_errors = 0
other_errors = 0
# 提取节点ID(简单的正则匹配)
import re
node_id_matches = re.findall(r'"(\d+)":', error_text) or re.findall(r"'(\d+)':", error_text)
if node_id_matches:
error_analysis["affected_nodes"] = list(set(node_id_matches))
# 连接相关错误的关键词(优先判断,因为结构性错误更重要)
connection_keywords = [
"connection", "input connection", "required input", "missing input",
"not connected", "no connection", "link", "output", "socket",
"missing_input", "invalid_connection", "connection_error"
]
# 参数相关错误的关键词
parameter_keywords = [
"value not in list", "invalid value", "not found in list",
"parameter value", "invalid parameter", "model not found",
"invalid image file", "value_not_in_list", "invalid_input"
]
# 计算错误类型出现次数
for keyword in connection_keywords:
if keyword in error_text:
connection_errors += error_text.count(keyword)
for keyword in parameter_keywords:
if keyword in error_text:
parameter_errors += error_text.count(keyword)
# 如果没有匹配到特定错误类型,检查是否有一般性错误指示
if connection_errors == 0 and parameter_errors == 0:
general_error_keywords = ["error", "failed", "exception", "invalid"]
for keyword in general_error_keywords:
if keyword in error_text:
other_errors += 1
break
# 根据错误类型决定使用哪个agent
if connection_errors > 0 and parameter_errors == 0 and other_errors == 0:
# 纯连接错误,使用专门的link_agent
error_analysis["error_type"] = "connection_error"
error_analysis["recommended_agent"] = "link_agent"
elif connection_errors > 0:
# 混合错误,优先处理连接问题
error_analysis["error_type"] = "mixed_connection_error"
error_analysis["recommended_agent"] = "link_agent"
elif parameter_errors > 0:
error_analysis["error_type"] = "parameter_error"
error_analysis["recommended_agent"] = "parameter_agent"
elif other_errors > 0:
error_analysis["error_type"] = "structural_error"
error_analysis["recommended_agent"] = "workflow_bugfix_default_agent"
else:
# 没有检测到明确的错误模式,使用默认agent
error_analysis["error_type"] = "unknown"
error_analysis["recommended_agent"] = "workflow_bugfix_default_agent"
# 添加错误详情(基于文本内容)
if connection_errors > 0:
error_analysis["error_details"].append({
"error_type": "connection_error",
"message": f"Detected {connection_errors} connection-related issues",
"details": "Connection or input/output related errors found"
})
if parameter_errors > 0:
error_analysis["error_details"].append({
"error_type": "parameter_error",
"message": f"Detected {parameter_errors} parameter-related issues",
"details": "Parameter value or configuration related errors found"
})
elif "error" in error_dict:
# 处理其他格式的错误
error_analysis = error_dict.get("error", {})
return json.dumps(error_analysis)
except Exception as e:
@@ -331,6 +341,7 @@ Start by validating the workflow to see its current state.""",
- Review missing_connections and possible_connections from the analysis
- Choose the most efficient combination of existing connections and new nodes
- Consider connection_summary to understand the scope of work needed
- Do not lose or modify parameters that are not reporting errors
4. **Verification and handoff**:
- After applying fixes: TRANSFER to ComfyUI-Debug-Coordinator for verification
@@ -408,7 +419,7 @@ Start by validating the workflow to see its current state.""",
* wan2.1 system (examples: wan2.1_base, wan2.1_t2v, etc.)
* wan2.2 system (examples: wan2.2_t2v, wan2.2_iv2, wan2.2_kontext, wan2.2_redux, etc.)
- Match by model system first (SDXL/Flux/wan2.1/wan2.2), then by model category (fill/dev/base/t2v/iv2/kontext/redux)
- **System-specific component matching rules:**
- [Critical!] **System-specific component matching rules:**
* **Flux series**: Requires fixed system components - vae: ae.safetensors, DualCLIPLoader: clip_l.safetensors + t5xxl_fp16.safetensors or t5xxl_fp8.safetensors, type: flux. In DualCLIP and UNetLoader/Load Checkpoint, search by system+category (e.g., Flux-dev-fp8 can be replaced with similar Flux-dev)
* **SDXL series**: vae: sdxl_vae.safetensors or vae-fe-mse-840000-ema-pruned.safetensors (priority search by system: vae, category: sdxl/840000). Load checkpoint search by system: sdxl, category: similar name (e.g., SDXL-dreamshaper.safetensors where dreamshaper is the category)
- If similar model from same system exists, replace with most similar match
+3 -3
View File
@@ -2,7 +2,7 @@
Author: ai-business-hql qingli.hql@alibaba-inc.com
Date: 2025-06-16 16:50:17
LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
LastEditTime: 2025-07-31 20:16:19
LastEditTime: 2025-08-06 14:54:19
FilePath: /comfyui_copilot/backend/service/mcp-client.py
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
'''
@@ -69,7 +69,7 @@ async def comfyui_agent_invoke(messages: List[Dict[str, Any]], images: List[Imag
os.environ["OPENAI_BASE_URL"] = config.get("openai_base_url")
# 创建带有session_id的workflow_rewrite_agent实例
workflow_rewrite_agent_instance = create_workflow_rewrite_agent(session_id)
workflow_rewrite_agent_instance = create_workflow_rewrite_agent(session_id, config)
agent = create_agent(
name="ComfyUI-Copilot",
@@ -100,7 +100,7 @@ You must adhere to the following constraints to complete the task:
- **DEBUG Intent** - When the user's intent is to debug workflow execution problems, runtime errors, or troubleshoot failed workflows (keywords: "debug", "调试", "工作流报错", "执行失败", "workflow failed", "node error", "runtime error", "不能运行", "出错了"), respond with: "Please click the 🪲 button in the bottom right corner to trigger debug"
- **WORKFLOW REWRITE Intent** - [Critical!] When the user's intent is to functionally modify, enhance, or add NEW features to the current workflow (keywords: "修改当前工作流", "更新工作流", "在当前工作流中添加", "添加功能", "enhance current workflow", "modify workflow", "add to current workflow", "update current workflow", "添加LoRA", "加个upscale", "add upscaling"), you MUST handoff to the Workflow Rewrite Agent immediately. This is for feature enhancements, not error fixes.
- **WORKFLOW REWRITE Intent** - [Critical!] When the user's intent is to functionally modify, enhance, or add NEW features to the current workflow OR modify the current canvas (keywords: "修改当前工作流", "更新工作流", "在当前工作流中添加", "添加功能", "enhance current workflow", "modify workflow", "add to current workflow", "update current workflow", "添加LoRA", "加个upscale", "add upscaling", "修改当前画布", "更新画布", "在画布中", "modify current canvas", "update canvas", "change canvas"), you MUST handoff to the Workflow Rewrite Agent immediately. This is for feature enhancements, not error fixes.
- **ERROR MESSAGE ANALYSIS** - When a user pastes specific error text/logs (containing terms like "Failed", "Error", "Traceback", or stack traces), prioritize providing troubleshooting help rather than invoking search tools. Follow these steps:
1. Analyze the error to identify the root cause (error type, affected component, missing dependencies, etc.)
+54 -36
View File
@@ -73,34 +73,8 @@ async def find_matching_parameter_value(node_name: str, param_name: str, current
"is_file_related": False,
}
# 识别model相关错误
if (any(model_keyword in current_value.lower() for model_keyword in [
".ckpt", ".safetensors", ".pt", ".pth", ".bin", "checkpoint", "lora", "vae", "controlnet", "clip", "unet"
]) or any(model_keyword in param_name.lower() for model_keyword in [
"model", "checkpoint", "lora", "vae", "clip", "controlnet", "unet"
]) or any(model_keyword in error_lower for model_keyword in [
"model not found", "checkpoint", "missing model", "file not found"
])):
error_analysis["error_type"] = "model_missing"
error_analysis["is_model_related"] = True
return json.dumps({
"found_match": False,
"error_type": "model_missing",
"message": f"Missing model file: {current_value}",
"recommendation": "Check for available model replacements, otherwise download required.",
"next_action": "check_available_models_or_suggest_download",
"details": {
"node_name": node_name,
"param_name": param_name,
"missing_file": current_value,
"is_model_related": True,
"param_config": param_config
}
})
# 识别image文件相关错误
elif (any(img_ext in current_value.lower() for img_ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp"]) or
# 优先识别image文件相关错误(在model检测之前,因为image文件可能包含model关键词)
if (any(img_ext in current_value.lower() for img_ext in [".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp"]) or
param_name.lower() in ["image", "img", "picture", "photo"] or
any(img_keyword in error_lower for img_keyword in ["invalid image", "image file", "image not found"])):
error_analysis["error_type"] = "image_file_missing"
@@ -135,6 +109,32 @@ async def find_matching_parameter_value(node_name: str, param_name: str, current
"can_auto_fix": False
})
# 识别model相关错误(在image检测之后)
elif (any(model_keyword in current_value.lower() for model_keyword in [
".ckpt", ".safetensors", ".pt", ".pth", ".bin", "checkpoint", "lora", "vae", "controlnet", "clip", "unet"
]) or any(model_keyword in param_name.lower() for model_keyword in [
"model", "checkpoint", "lora", "vae", "clip", "controlnet", "unet"
]) or any(model_keyword in error_lower for model_keyword in [
"model not found", "checkpoint", "missing model", "file not found"
])):
error_analysis["error_type"] = "model_missing"
error_analysis["is_model_related"] = True
return json.dumps({
"found_match": False,
"error_type": "model_missing",
"message": f"Missing model file: {current_value}",
"recommendation": "Check for available model replacements, otherwise download required.",
"next_action": "check_available_models_or_suggest_download",
"details": {
"node_name": node_name,
"param_name": param_name,
"missing_file": current_value,
"is_model_related": True,
"param_config": param_config
}
})
# 处理枚举类型的参数(原有逻辑,但增强)
elif isinstance(param_config, list) and len(param_config) > 0:
available_values = param_config
@@ -250,9 +250,6 @@ async def find_matching_parameter_value(node_name: str, param_name: str, current
async def get_model_files(model_type: str = "checkpoints") -> str:
"""获取可用的模型文件列表"""
try:
# 获取所有节点信息
object_info = await get_object_info()
# 定义模型类型到节点的映射
model_type_mapping = {
"checkpoints": ["CheckpointLoaderSimple", "CheckpointLoader"],
@@ -267,15 +264,36 @@ async def get_model_files(model_type: str = "checkpoints") -> str:
# 查找对应的节点
model_files = {}
for node_name in model_type_mapping.get(model_type.lower(), []):
if node_name in object_info:
node_info = object_info[node_name]
if 'input' in node_info:
try:
# 使用 get_object_info_by_class 获取单个节点信息,减少数据量
node_data = await get_object_info_by_class(node_name)
# 处理不同的返回格式
node_info = None
if node_name in node_data:
# 格式:{"NodeName": {...}}
node_info = node_data[node_name]
elif 'input' in node_data:
# 格式:直接返回节点信息 {...}
node_info = node_data
if node_info and 'input' in node_info:
# 查找包含文件列表的参数
for input_type in ['required', 'optional']:
if input_type in node_info['input']:
for param_name, param_config in node_info['input'][input_type].items():
if isinstance(param_config, tuple) and isinstance(param_config[0], list) and len(param_config[0]) > 0:
model_files[f"{node_name}.{param_name}"] = param_config[0]
# 检查参数配置格式:[file_list, {...}] 或 [file_list]
if isinstance(param_config, list) and len(param_config) > 0:
if isinstance(param_config[0], list) and len(param_config[0]) > 0:
# 检查是否为文件列表(包含文件扩展名或路径)
file_list = param_config[0]
if any(isinstance(item, str) and ('.' in item or '/' in item) for item in file_list):
model_files[f"{node_name}.{param_name}"] = file_list
except Exception as e:
# 单个节点查询失败,继续处理其他节点
print(f"Failed to get info for node {node_name}: {e}")
continue
if model_files:
return json.dumps({
+61 -797
View File
@@ -2,7 +2,7 @@
Author: ai-business-hql qingli.hql@alibaba-inc.com
Date: 2025-07-24 17:10:23
LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
LastEditTime: 2025-07-31 11:20:37
LastEditTime: 2025-08-06 19:32:27
FilePath: /comfyui_copilot/backend/service/workflow_rewrite_agent.py
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
'''
@@ -11,17 +11,57 @@ from agents.tool import function_tool
import json
import time
import uuid
from agents.tool import function_tool
import os
from typing import Dict, Any
from ..agent_factory import create_agent
from ..utils.globals import get_language
from ..service.workflow_rewrite_tools import *
@function_tool
def get_rewrite_expert_by_name(name_list: list[str]) -> str:
"""根据经验名称来获取工作流改写专家经验"""
result = ""
with open(os.path.join(os.path.dirname(__file__), "..", "data", "workflow_rewrite_expert.json"), "r") as f:
data = json.load(f)
for name in name_list:
for item in data:
if item["name"] == name:
result += f'### {name}({item["description"]}):\n{item["content"]}\n'
return result
def create_workflow_rewrite_agent(session_id: str):
"""创建带有session_id的workflow_rewrite_agent实例"""
def get_rewrite_export_schema() -> dict:
"""获取工作流改写专家经验schema"""
expert_schema = []
with open(os.path.join(os.path.dirname(__file__), "..", "data", "workflow_rewrite_expert.json"), "r") as f:
data = json.load(f)
for item in data:
expert_schema.append({
"name": item["name"],
"description": item["description"]
})
return expert_schema
def create_workflow_rewrite_agent(session_id: str, config: Dict[str, Any] = None):
"""创建带有session_id和config的workflow_rewrite_agent实例"""
language = get_language()
# 导入workflow_rewrite_tools并设置配置
from .workflow_rewrite_tools import get_workflow_data_from_config, update_workflow, remove_node, get_node_info
from ..service.database import get_workflow_data
# 创建带有配置的工具函数
@function_tool
def get_current_workflow_with_config(session_id: str) -> str:
"""获取当前session的工作流数据"""
workflow_data = get_workflow_data_from_config(config) if config else get_workflow_data(session_id)
if not workflow_data:
return json.dumps({"error": "No workflow data found for this session"})
return json.dumps(workflow_data)
return create_agent(
name="Workflow Rewrite Agent",
model="us.anthropic.claude-sonnet-4-20250514-v1:0",
@@ -35,809 +75,33 @@ def create_workflow_rewrite_agent(session_id: str):
**当前Session ID:** {}""".format(language, session_id) + """
## 主要处理场景
### 文生图场景优化
1. **LoRA集成**:在现有工作流中添加LoRA节点,确保与现有模型和提示词节点正确连接
在checkpoint节点后添加LoRA节点。
{
"1": {
"inputs": {
"lora_name": "DOG.safetensors",
"strength_model": 1,
"strength_clip": 1,
"model": [
"2",
0
],
"clip": [
"2",
1
]
},
"class_type": "LoraLoader",
"_meta": {
"title": "Load LoRA"
}
}
在checkpoint节点后添加vae encode节点
{
"2": {
"inputs": {
"ckpt_name": "Flux_Kontext_dev_.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"vae": [
"2",
2
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
}
}
2. **后处理增强**:
- 在Preview Image或Save Image节点后添加高清放大功能(如Real-ESRGAN、ESRGAN等)
- 添加图像缩放节点
{
"1": {
"inputs": {
"width": 512,
"height": 512,
"interpolation": "nearest",
"method": "stretch",
"condition": "always",
"multiple_of": 0
},
"class_type": "ImageResize+",
"_meta": {
"title": "🔧 Image Resize"
}
}
}
- 添加图像尺寸调整节点
{
"2": {
"inputs": {
"aspect_ratio": "original",
"proportional_width": 2,
"proportional_height": 1,
"fit": "letterbox",
"method": "lanczos",
"round_to_multiple": "8",
"scale_to_longest_side": false,
"longest_side": 1024
},
"class_type": "LayerUtility: ImageScaleByAspectRatio",
"_meta": {
"title": "LayerUtility: ImageScaleByAspectRatio"
}
}
}
-添加图像放大节点
"11": {
"inputs": {
"width": 512,
"height": 512,
"upscale_method": "nearest-exact",
"keep_proportion": false,
"divisible_by": 2,
"crop": "disabled",
"image": [
"12",
0
]
},
"class_type": "ImageResizeKJ",
"_meta": {
"title": "Resize Image"
}
},
-添加VAEdecode节点
"12": {
"inputs": {},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
}
}
3. **提示词优化**:
- 修改现有提示词节点的内容(提示词应该在(CLIP Text Encode Prompt或Text _O节点内编辑)
- 添加单独的提示词输入节点
{
"12": {
"inputs": {
"text": [
"13",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"13": {
"inputs": {
"delimiter": ", ",
"clean_whitespace": "true",
"text_a": [
"15",
0
],
"text_b": [
"16",
0
]
},
"class_type": "Text Concatenate",
"_meta": {
"title": "Text Concatenate"
}
},
"15": {
"inputs": {
"text": ""
},
"class_type": "Text _O",
"_meta": {
"title": "Text _O"
}
},
"16": {
"inputs": {
"text": ""
},
"class_type": "Text _O",
"_meta": {
"title": "Text _O"
}
}
}
### 图生图场景优化
1. **LoRA增强**:为图生图工作流添加LoRA支持,保证与图像输入节点兼容
-在checkpoint节点后添加LoRA节点。(与文生图添加lora节点的方式保持一致)
2. **图像反推功能**:
- 添加图像反推节点(如CLIP Interrogator)
{
"11": {
"inputs": {
"prompt_mode": "fast",
"image_analysis": "off"
},
"class_type": "ClipInterrogator",
"_meta": {
"title": "Clip Interrogator ♾️Mixlab"
}
}
}
- 添加更复杂或者更好用的图像反推节点(加载图片使用florence2进行反推)
{
"6": {
"inputs": {
"image": "06.JPG"
},
"class_type": "LoadImage",
"_meta": {
"title": "$image.image!:The image to analyze, must be a url"
}
},
"10": {
"inputs": {
"model": "microsoft/Florence-2-large",
"precision": "fp16",
"attention": "sdpa"
},
"class_type": "DownloadAndLoadFlorence2Model",
"_meta": {
"title": "DownloadAndLoadFlorence2Model"
}
},
"11": {
"inputs": {
"text_input": "",
"task": "more_detailed_caption",
"fill_mask": true,
"keep_model_loaded": false,
"max_new_tokens": 1024,
"num_beams": 3,
"do_sample": true,
"output_mask_select": "",
"seed": 1098631327477633,
"image": [
"6",
0
],
"florence2_model": [
"10",
0
]
},
"class_type": "Florence2Run",
"_meta": {
"title": "Florence2Run"
}
},
"18": {
"inputs": {
"anything": [
"11",
2
]
},
"class_type": "easy showAnything",
"_meta": {
"title": "Show Any"
}
},
"20": {
"inputs": {
"value": "Generate high-quality text descriptions from images using local Florence model.\n \n Main use cases:\n 1. **Reverse image prompt generation**:\n - When users upload an image and want to get prompts for AI art generation\n - Analyzes visual elements, style, composition, etc. to generate prompts for Stable Diffusion, DALL-E, and other models\n - In this case, return the tool's raw output directly to users without any modification or summary\n - Users can directly use these prompts for image generation\n 2. **Image content understanding**:\n - When users ask about specific content, objects, scenes, people, etc. in the image\n - Need to understand the semantic content of the image and answer users' specific questions\n - In this case, combine tool output with conversation context to give users contextually appropriate natural responses\n - Don't return raw output directly, but provide targeted replies based on understanding results"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "MCP"
}
}
}
3. **ControlNet集成**:(preprocessor可选canny、depth等参数,模型样式选择,Controlnet开始、结束权重)
{
"22": {
"inputs": {
"strength": 1,
"start_percent": 0,
"end_percent": 1,
"control_net": [
"23",
0
],
"image": [
"24",
0
]
},
"class_type": "ControlNetApplyAdvanced",
"_meta": {
"title": "Apply ControlNet"
}
},
"23": {
"inputs": {
"control_net_name": "ControlNet-Standard-Lineart-for-SDXL.safetensors"
},
"class_type": "ControlNetLoader",
"_meta": {
"title": "Load ControlNet Model"
}
},
"24": {
"inputs": {
"preprocessor": "none",
"resolution": 512
},
"class_type": "AIO_Preprocessor",
"_meta": {
"title": "AIO Aux Preprocessor"
}
}
}
4. **高级图像处理**:
-给我一个扩图链路(包括图像加载和图像输出)
- 添加图像高清放大链路
{
"58": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"59": {
"inputs": {
"clip_name1": "t5xxl_fp8_e4m3fn.safetensors",
"clip_name2": "clip_l.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"60": {
"inputs": {
"unet_name": "F.1-dev-fp8.safetensors",
"weight_dtype": "fp8_e4m3fn"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"175": {
"inputs": {
"upscale_by": 2.0000000000000004,
"seed": 203184385867926,
"steps": 10,
"cfg": 3,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 0.34,
"mode_type": "Linear",
"tile_width": 512,
"tile_height": 512,
"mask_blur": 8,
"tile_padding": 32,
"seam_fix_mode": "None",
"seam_fix_denoise": 1,
"seam_fix_width": 64,
"seam_fix_mask_blur": 8,
"seam_fix_padding": 16,
"force_uniform_tiles": true,
"tiled_decode": false,
"image": [
"263",
0
],
"model": [
"266",
0
],
"positive": [
"267",
0
],
"negative": [
"271",
0
],
"vae": [
"269",
0
],
"upscale_model": [
"270",
0
]
},
"class_type": "UltimateSDUpscale",
"_meta": {
"title": "Ultimate SD Upscale"
}
},
"176": {
"inputs": {
"text": [
"226",
2
],
"clip": [
"59",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"178": {
"inputs": {
"model_name": "4xNomos8kSCHAT-L.pth"
},
"class_type": "UpscaleModelLoader",
"_meta": {
"title": "Load Upscale Model"
}
},
"188": {
"inputs": {
"guidance": 30,
"conditioning": [
"213",
0
]
},
"class_type": "FluxGuidance",
"_meta": {
"title": "FluxGuidance"
}
},
"189": {
"inputs": {
"conditioning": [
"176",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"212": {
"inputs": {
"type": "tile",
"control_net": [
"214",
0
]
},
"class_type": "SetUnionControlNetType",
"_meta": {
"title": "SetUnionControlNetType"
}
},
"213": {
"inputs": {
"strength": 0.30000000000000004,
"start_percent": 0,
"end_percent": 1,
"positive": [
"176",
0
],
"negative": [
"189",
0
],
"control_net": [
"212",
0
],
"image": [
"215",
0
],
"vae": [
"58",
0
]
},
"class_type": "ControlNetApplyAdvanced",
"_meta": {
"title": "Apply ControlNet"
}
},
"214": {
"inputs": {
"control_net_name": "FLUX.1-dev-ControlNet-Union-Pro.safetensors"
},
"class_type": "ControlNetLoader",
"_meta": {
"title": "Load ControlNet Model"
}
},
"215": {
"inputs": {
"preprocessor": "TTPlanet_TileSimple_Preprocessor",
"resolution": [
"217",
0
],
"image": [
"272",
0
]
},
"class_type": "AIO_Preprocessor",
"_meta": {
"title": "AIO Aux Preprocessor"
}
},
"216": {
"inputs": {
"image": [
"272",
0
]
},
"class_type": "GetImageSize+",
"_meta": {
"title": "🔧 Get Image Size"
}
},
"217": {
"inputs": {
"image_gen_width": [
"216",
0
],
"image_gen_height": [
"216",
1
],
"resize_mode": "Just Resize",
"original_image": [
"272",
0
]
},
"class_type": "PixelPerfectResolution",
"_meta": {
"title": "Pixel Perfect Resolution"
}
},
"226": {
"inputs": {
"text_input": "",
"task": "region_caption",
"fill_mask": true,
"keep_model_loaded": false,
"max_new_tokens": 1024,
"num_beams": 3,
"do_sample": true,
"output_mask_select": "",
"seed": 204052510973001,
"image": [
"272",
0
],
"florence2_model": [
"227",
0
]
},
"class_type": "Florence2Run",
"_meta": {
"title": "Florence2Run"
}
},
"227": {
"inputs": {
"model": "Florence-2-large",
"precision": "fp16",
"attention": "sdpa"
},
"class_type": "Florence2ModelLoader",
"_meta": {
"title": "Florence2ModelLoader"
}
},
"230": {
"inputs": {
"text": "home appliance<loc_669><loc_453><loc_902><loc_646><loc_118><loc_452><loc_341><loc_646>",
"anything": [
"226",
2
]
},
"class_type": "easy showAnything",
"_meta": {
"title": "Show Any"
}
},
"240": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"175",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"263": {
"inputs": {
"anything": [
"272",
0
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
},
"266": {
"inputs": {
"anything": [
"60",
0
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
},
"267": {
"inputs": {
"anything": [
"188",
0
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
},
"269": {
"inputs": {
"anything": [
"58",
0
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
},
"270": {
"inputs": {
"anything": [
"178",
0
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
},
"271": {
"inputs": {
"anything": [
"213",
1
]
},
"class_type": "easy cleanGpuUsed",
"_meta": {
"title": "Clean VRAM Used"
}
},
"272": {
"inputs": {
"image": "234.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image"
}
}
}
- 配置图像缩放和裁剪功能
{
"46": {
"inputs": {
"width": [
"47",
1
],
"height": [
"47",
2
],
"position": "top-left",
"x_offset": 0,
"y_offset": 0,
"image": [
"47",
0
]
},
"class_type": "ImageCrop+",
"_meta": {
"title": "🔧 Image Crop"
}
},
"47": {
"inputs": {
"width": 512,
"height": 512,
"interpolation": "nearest",
"method": "stretch",
"condition": "always",
"multiple_of": 0
},
"class_type": "ImageResize+",
"_meta": {
"title": "🔧 Image Resize"
}
},
"49": {
"inputs": {
"images": [
"46",
0
]
},
"class_type": "PreviewImage",
"_meta": {
"title": "Preview Image"
}
}
}
5. **智能抠图**:
- 添加背景移除节点(如SAM、U²-Net等)
- 添加背景移除或抠图节点
{
"7": {
"inputs": {
"rem_mode": "RMBG-1.4",
"image_output": "Preview",
"save_prefix": "ComfyUI",
"torchscript_jit": false,
"add_background": "none",
"refine_foreground": false
},
"class_type": "easy imageRemBg",
"_meta": {
"title": "Image Remove Bg"
}
}
}
- 添加SAM抠图节点
{
"8": {
"inputs": {
"prompt": "",
"threshold": 0.3,
"sam_model": [
"9",
0
],
"grounding_dino_model": [
"10",
0
]
},
"class_type": "GroundingDinoSAMSegment (segment anything)",
"_meta": {
"title": "GroundingDinoSAMSegment (segment anything)"
}
},
"9": {
"inputs": {
"model_name": "sam_vit_h (2.56GB)"
},
"class_type": "SAMModelLoader (segment anything)",
"_meta": {
"title": "SAMModelLoader (segment anything)"
}
},
"10": {
"inputs": {
"model_name": "GroundingDINO_SwinT_OGC (694MB)"
},
"class_type": "GroundingDinoModelLoader (segment anything)",
"_meta": {
"title": "GroundingDinoModelLoader (segment anything)"
}
}
}
{}
""".format(json.dumps(get_rewrite_export_schema())) + """
你可以根据用户的需求,从上面的专家经验中选择一个或多个经验,并根据经验内容进行工作流改写。
## 操作原则
- **保持兼容性**:确保修改后的工作流与现有节点兼容
- **优化连接**:正确设置节点间的输入输出连接
- **连线完整性**:修改工作流时必须确保所有节点的连线关系完整,不遗漏任何必要的输入输出连接
* 检查每个节点的必需输入是否已连接
* 对于未连接的必需输入,优先寻找类型匹配的现有节点输出进行连接
* 如果找不到合适的现有输出,则创建适当的输入节点(如常量节点、加载节点等)
* 确保连接的参数类型完全匹配,避免类型不兼容的连接
- **连线检查**:在添加、删除或修改节点时,务必检查所有相关的输入和输出连接是否正确配置
- **连接关系维护**:修改节点时必须保持原有的连接逻辑,确保数据流向正确
- **类型严格匹配**:在进行任何连线操作时,必须严格验证输入输出类型匹配
* 在修改连线前,先使用get_node_info()获取节点的完整输入输出规格信息
* 仔细检查源节点的输出类型(output_type)与目标节点的输入类型(input_type)
* 如果类型不匹配,寻找正确的源节点或添加类型转换节点
- **性能考虑**:避免不必要的重复节点,优化工作流执行效率
- **用户友好**:保持工作流结构清晰,便于用户理解和后续修改
- **错误处理**:在修改过程中检查潜在的配置错误,提供修正建议
**Tool Usage Guidelines:**
- get_current_workflow(): Get current workflow from checkpoint or session
- remove_node(): Use for incompatible or problematic nodes
- update_workflow(): Use to save your changes (ALWAYS call this after fixes)
- get_node_info(): Get detailed node information and verify input/output types before connecting
## 响应格式
@@ -845,7 +109,7 @@ def create_workflow_rewrite_agent(session_id: str):
始终以用户的实际需求为导向,提供专业、准确、高效的工作流改写服务。
""",
tools=[get_current_workflow, get_node_info, update_workflow, remove_node],
tools=[get_rewrite_expert_by_name, get_current_workflow_with_config, get_node_info, update_workflow, remove_node],
)
# 保持向后兼容性的默认实例
+41 -3
View File
@@ -6,10 +6,47 @@ from typing import Dict, Any, Optional, List
from agents.agent import Agent
from agents.tool import function_tool
from ..utils.string_utils import error_format
from ..service.database import get_workflow_data, save_workflow_data
from ..service.database import get_workflow_data, save_workflow_data, get_workflow_data_ui, get_workflow_data_by_id
from ..utils.comfy_gateway import get_object_info
def get_workflow_data_from_config(config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""获取工作流数据,优先使用checkpoint_id,如果没有则使用session_id"""
workflow_checkpoint_id = config.get('workflow_checkpoint_id')
session_id = config.get('session_id')
if workflow_checkpoint_id:
try:
checkpoint_data = get_workflow_data_by_id(workflow_checkpoint_id)
if checkpoint_data and checkpoint_data.get('workflow_data'):
return checkpoint_data['workflow_data']
except Exception as e:
print(f"Failed to get workflow data from checkpoint {workflow_checkpoint_id}: {str(e)}")
if session_id:
return get_workflow_data(session_id)
return None
def get_workflow_data_ui_from_config(config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""获取工作流UI数据,优先使用checkpoint_id,如果没有则使用session_id"""
workflow_checkpoint_id = config.get('workflow_checkpoint_id')
session_id = config.get('session_id')
if workflow_checkpoint_id:
try:
checkpoint_data = get_workflow_data_by_id(workflow_checkpoint_id)
if checkpoint_data and checkpoint_data.get('workflow_data_ui'):
return checkpoint_data['workflow_data_ui']
except Exception as e:
print(f"Failed to get workflow UI data from checkpoint {workflow_checkpoint_id}: {str(e)}")
if session_id:
return get_workflow_data_ui(session_id)
return None
@function_tool
def get_current_workflow(session_id: str) -> str:
"""获取当前session的工作流数据"""
@@ -47,7 +84,7 @@ def save_checkpoint_before_modification(session_id: str, action_description: str
checkpoint_id = save_workflow_data(
session_id,
current_workflow,
workflow_data_ui=None, # UI format not available in tools
workflow_data_ui=get_workflow_data_ui(session_id),
attributes={
"checkpoint_type": "workflow_rewrite_start",
"description": f"Checkpoint before {action_description}",
@@ -111,7 +148,8 @@ def update_workflow(session_id: str, workflow_data: str) -> str:
"ext": ext_data
})
except Exception as e:
return json.dumps({"error": f"Failed to update workflow: {str(e)}"})
print(f"Failed to update workflow: {str(e)}")
return json.dumps({"error": f"Failed to update workflow"})
@function_tool
def remove_node(session_id: str, node_id: str) -> str:
+5
View File
@@ -0,0 +1,5 @@
def error_format(e: Exception) -> str:
error_message = str(e).replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')
# 移除可能导致JSON解析错误的控制字符
error_message = ''.join(char for char in error_message if ord(char) >= 32 or char in '\n\r\t')
return error_message
@@ -1,7 +1,7 @@
function getImportPath(filename) {
return `./${filename}`;
}
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-CSy2RX18.js","copilot_web/vendor-markdown-BAJQc3l1.js","copilot_web/vendor-react-Dixhfmvb.js","copilot_web/message-components-Cf5ztmsp.js","copilot_web/input.js","copilot_web/assets/input-BDTaXBTQ.css","copilot_web/fonts.css"].map(path => {
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-BGZdyRxi.js","copilot_web/vendor-markdown-BAJQc3l1.js","copilot_web/vendor-react-Dixhfmvb.js","copilot_web/message-components-DPTrVTCp.js","copilot_web/input.js","copilot_web/assets/input-BDTaXBTQ.css","copilot_web/fonts.css"].map(path => {
const apiBase = window.comfyAPI?.api?.api?.api_base;
if (apiBase) {
// 有 API base 时,使用完整路径
@@ -11,4 +11,4 @@ function getImportPath(filename) {
return `./${path}`;
}
}))))=>i.map(i=>d[i]);
import{_ as a}from"./input.js";import{j as e}from"./vendor-markdown-BAJQc3l1.js";import{r as t,R as i}from"./vendor-react-Dixhfmvb.js";import{C as n}from"./message-components-Cf5ztmsp.js";const s={EXPLAIN_NODE:"copilot:explain-node",TOOLBOX_USAGE:"copilot:toolbox-usage",TOOLBOX_PARAMETERS:"copilot:toolbox-parameters",TOOLBOX_DOWNSTREAMNODES:"copilot:toolbox-downstreamnodes"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-CSy2RX18.js")),__vite__mapDeps([0,1,2,3,4,5,6])).then(o=>({default:o.default})));function c(){const[o,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),e.jsx(n,{children:e.jsx("div",{className:"h-full w-full flex flex-col",children:e.jsx(t.Suspense,{fallback:e.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:e.jsx(d,{visible:!0,triggerUsage:o,onUsageTriggered:()=>r(!1)})})})})}const O=Object.freeze(Object.defineProperty({__proto__:null,default:c},Symbol.toStringTag,{value:"Module"}));export{O as A,s as C};
import{_ as a}from"./input.js";import{j as e}from"./vendor-markdown-BAJQc3l1.js";import{r as t,R as i}from"./vendor-react-Dixhfmvb.js";import{C as n}from"./message-components-DPTrVTCp.js";const s={EXPLAIN_NODE:"copilot:explain-node",TOOLBOX_USAGE:"copilot:toolbox-usage",TOOLBOX_PARAMETERS:"copilot:toolbox-parameters",TOOLBOX_DOWNSTREAMNODES:"copilot:toolbox-downstreamnodes"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-BGZdyRxi.js")),__vite__mapDeps([0,1,2,3,4,5,6])).then(o=>({default:o.default})));function c(){const[o,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),e.jsx(n,{children:e.jsx("div",{className:"h-full w-full flex flex-col",children:e.jsx(t.Suspense,{fallback:e.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:e.jsx(d,{visible:!0,triggerUsage:o,onUsageTriggered:()=>r(!1)})})})})}const O=Object.freeze(Object.defineProperty({__proto__:null,default:c},Symbol.toStringTag,{value:"Module"}));export{O as A,s as C};
@@ -1,5 +1,5 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as r}from"./vendor-markdown-BAJQc3l1.js";import{r as a}from"./vendor-react-Dixhfmvb.js";import{B as I,R as E,h as f,a as h,W as x}from"./message-components-Cf5ztmsp.js";function B({content:g,name:m="Assistant",avatar:A,onAddMessage:b,onUpdateMessage:c,onFinishLoad:k,messageId:y}){const[d,_]=a.useState(!1),[D,O]=a.useState(""),[J,w]=a.useState(!1),[S,v]=a.useState(null);a.useEffect(()=>{k?.()},[]);const s=a.useMemo(()=>{try{return JSON.parse(g)}catch(e){return console.error("Failed to parse message content:",e),null}},[g]),p=a.useMemo(()=>{if(!s||!s.ext)return null;const e=s.ext.find(t=>t.type==="debug_start_checkpoint"||t.type==="debug_checkpoint"&&t.data?.checkpoint_type==="debug_start");return e&&e.data&&e.data.checkpoint_id?e.data.checkpoint_id:null},[s])||S,j=async()=>{if(d)return;const e=f(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
import{j as r}from"./vendor-markdown-BAJQc3l1.js";import{r as a}from"./vendor-react-Dixhfmvb.js";import{B as I,R as E,h as f,a as h,W as x}from"./message-components-DPTrVTCp.js";function B({content:g,name:m="Assistant",avatar:A,onAddMessage:b,onUpdateMessage:c,onFinishLoad:k,messageId:y}){const[d,_]=a.useState(!1),[D,O]=a.useState(""),[J,w]=a.useState(!1),[S,v]=a.useState(null);a.useEffect(()=>{k?.()},[]);const s=a.useMemo(()=>{try{return JSON.parse(g)}catch(e){return console.error("Failed to parse message content:",e),null}},[g]),p=a.useMemo(()=>{if(!s||!s.ext)return null;const e=s.ext.find(t=>t.type==="debug_start_checkpoint"||t.type==="debug_checkpoint"&&t.data?.checkpoint_type==="debug_start");return e&&e.data&&e.data.checkpoint_id?e.data.checkpoint_id:null},[s])||S,j=async()=>{if(d)return;const e=f(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
`,ext:[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};b?.(t);try{await N(),await C(e)}finally{_(!1)}},N=async()=>{try{const e=await h.graphToPrompt(),t=localStorage.getItem("sessionId")||"";if(t&&e){const o=await x.saveWorkflowCheckpoint(t,e.output,e.workflow,"debug_start");if(v(o.version_id),console.log(`Saved debug start checkpoint: ${o.version_id}`),c&&s){const n=[...s.ext||[]].filter(u=>u.type!=="debug_start_checkpoint"&&!(u.type==="debug_checkpoint"&&u.data?.checkpoint_type==="debug_start"));n.push({type:"debug_start_checkpoint",data:{checkpoint_id:o.version_id,checkpoint_type:"debug_start"}});const l={id:y||f(),role:"ai",content:JSON.stringify({text:s.text,ext:n}),format:"debug_guide",name:"Assistant"};c(l)}}}catch(e){console.error("Failed to save checkpoint before debug:",e)}},C=async e=>{try{const t=await h.graphToPrompt();let o="",i=null;for await(const n of x.streamDebugAgent(t))if(n.text){o=n.text,n.ext&&(i=n.ext);const l={id:e,role:"ai",content:JSON.stringify({text:o,ext:i||[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};c?.(l)}c?.({id:e,role:"ai",content:JSON.stringify({text:o,ext:i||[]}),format:"markdown",name:"Assistant",finished:!0,debugGuide:!0})}catch(t){console.error("Error calling debug agent:",t),w(!1);const i={id:e,role:"ai",content:JSON.stringify({text:`I encountered an error while analyzing your workflow: ${t.message||"Unknown error"}`,ext:[]}),format:"markdown",name:"Assistant",finished:!0};c?.(i)}};return s?r.jsx(I,{name:m,children:r.jsxs("div",{className:"bg-gray-100 pt-4 px-4 pb-3 rounded-lg",children:[r.jsx("div",{className:"flex justify-between items-start",children:r.jsx("p",{className:"text-gray-700 text-sm flex-1",children:s.text})}),r.jsx("div",{className:"flex justify-end mt-4",children:r.jsx("button",{onClick:j,disabled:d,className:`px-4 py-2 text-[#fff] text-sm rounded-md shadow-[0_2px_8px_rbga(79, 140, 255, 0.15)] hover:!shadow-[0_4px_16px_rbga(79, 140, 255, 0.25)] hover:!scale-[1.05] transition-colors ${d?"bg-gray-400 cursor-not-allowed":"bg-debug-btn hover:!bg-debug-btn-hover"}`,children:d?"Analyzing...":"Debug Errors"})}),r.jsx("div",{className:"flex justify-end mt-2",children:!!p&&r.jsx("div",{className:"ml-2 flex-shrink-0",children:r.jsx(E,{checkpointId:p,onRestore:()=>{console.log("Workflow restored from checkpoint")}})})})]})}):null}export{B as DebugGuide};
@@ -1,4 +1,4 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as e}from"./vendor-markdown-BAJQc3l1.js";import{b as u,c as w,B as g,R as k}from"./message-components-Cf5ztmsp.js";import{r as x}from"./vendor-react-Dixhfmvb.js";const v=i=>{const{title:a="",isWorkflowUpdate:p=!1,className:c="",onFinishLoad:d,children:f}=i,[n,s]=x.useState(!1);return x.useEffect(()=>{d?.()},[]),e.jsxs("div",{className:`relative rounded-lg flex flex-col debug-collapsible-card ${c} ${n?"":"h-[200px]"}`,children:[e.jsx("div",{className:"card-border rounded-lg"}),e.jsxs("div",{className:"flex flex-1 justify-between items-center pb-2 border-b border-[#29292f]",children:[e.jsx("div",{children:typeof a=="string"?e.jsx("h3",{className:"text-sm text-[#fff] font-medium",children:a}):a}),e.jsx("button",{onClick:()=>{s(!n)},children:n?e.jsx(u,{color:"#E5E7EB"}):e.jsx(w,{color:"#E5E7EB"})})]}),e.jsx("div",{className:"overflow-hidden",children:f})]})};function y({content:i,name:a="Assistant",avatar:p,format:c="markdown",onFinishLoad:d}){const f=s=>c==="markdown"?s.replace(/\*\*(.*?)\*\*/g,"<strong>$1</strong>").replace(/\*(.*?)\*/g,"<em>$1</em>").replace(/`(.*?)`/g,'<code class="bg-gray-100 px-1 rounded">$1</code>').replace(/\n/g,"<br/>"):s,n=()=>{let s=null,r=!1,o;try{if(o=JSON.parse(i),o.ext){let t=o.ext.find(l=>l.type==="workflow_rewrite_checkpoint"||l.type==="debug_checkpoint"&&l.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(s=t.data.checkpoint_id,r=!0):(t=o.ext.find(l=>l.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(s=t.data.version_id,r=!0):(t=o.ext.find(l=>l.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(s=t.data.checkpoint_id,r=!1))),o.ext.find(l=>l.type==="workflow_update")&&(r=!0)}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const h=r?e.jsxs("div",{className:"flex items-center text-green-500",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"currentColor",viewBox:"0 0 20 20",children:e.jsx("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Successfully"})]}):e.jsxs("div",{className:"flex items-center text-[#fff]",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"28108",fill:"currentColor",children:e.jsx("path",{d:"M401.048025 844.855924c0 20.341281 16.643052 36.984333 36.984333 36.984333l147.936307 0c20.341281 0 36.984333-16.643052 36.984333-36.984333L622.952998 807.872614 401.048025 807.872614 401.048025 844.855924zM512 142.159744c-142.943596 0-258.888282 115.944686-258.888282 258.888282 0 88.021729 44.011376 165.503405 110.951975 212.288964l0 83.58365c0 20.341281 16.643052 36.984333 36.984333 36.984333l221.903949 0c20.341281 0 36.984333-16.643052 36.984333-36.984333l0-83.58365c66.941622-46.784536 110.951975-124.266212 110.951975-212.288964C770.888282 258.104429 654.943596 142.159744 512 142.159744zM617.588827 552.682561l-31.621185 22.005176 0 85.248569L438.031335 659.936307l0-85.063351L406.41015 552.866756c-49.743938-34.764781-79.33079-91.350544-79.33079-151.634536 0-101.890598 83.029018-184.92064 184.92064-184.92064s184.92064 83.029018 184.92064 184.92064C696.919617 461.332017 667.332764 517.91778 617.588827 552.682561z","p-id":"28277"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Finished"})]}),m=r?e.jsx("div",{className:"mt-3 text-xs text-gray-700",children:"💡 If you're not satisfied with the changes, click the restore button to revert to the previous version."}):null;return e.jsxs("div",{className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(v,{title:h,isWorkflowUpdate:r,className:"p-4",onFinishLoad:d,children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[c==="markdown"?e.jsx("div",{className:"text-[#fff]/70 text-sm leading-relaxed h-full",dangerouslySetInnerHTML:{__html:f(o?.text||"")}}):e.jsx("pre",{className:"whitespace-pre-wrap text-[#fff]/70 text-sm leading-relaxed h-full",children:o?.text||""}),m]})}),e.jsx("div",{className:"flex justify-end mt-2",children:!!s&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(k,{checkpointId:s,onRestore:()=>{console.log(`Workflow restored from ${r?"workflow update":"debug"} checkpoint`)},title:r?`Restore to this version (Version ${s})`:`Restore checkpoint ${s}`})})})]})};return e.jsx(g,{name:a,children:n()})}export{y as DebugResult};
import{j as e}from"./vendor-markdown-BAJQc3l1.js";import{b as u,c as w,B as g,R as k}from"./message-components-DPTrVTCp.js";import{r as x}from"./vendor-react-Dixhfmvb.js";const v=i=>{const{title:a="",isWorkflowUpdate:p=!1,className:c="",onFinishLoad:d,children:f}=i,[n,s]=x.useState(!1);return x.useEffect(()=>{d?.()},[]),e.jsxs("div",{className:`relative rounded-lg flex flex-col debug-collapsible-card ${c} ${n?"":"h-[200px]"}`,children:[e.jsx("div",{className:"card-border rounded-lg"}),e.jsxs("div",{className:"flex flex-1 justify-between items-center pb-2 border-b border-[#29292f]",children:[e.jsx("div",{children:typeof a=="string"?e.jsx("h3",{className:"text-sm text-[#fff] font-medium",children:a}):a}),e.jsx("button",{onClick:()=>{s(!n)},children:n?e.jsx(u,{color:"#E5E7EB"}):e.jsx(w,{color:"#E5E7EB"})})]}),e.jsx("div",{className:"overflow-hidden",children:f})]})};function y({content:i,name:a="Assistant",avatar:p,format:c="markdown",onFinishLoad:d}){const f=s=>c==="markdown"?s.replace(/\*\*(.*?)\*\*/g,"<strong>$1</strong>").replace(/\*(.*?)\*/g,"<em>$1</em>").replace(/`(.*?)`/g,'<code class="bg-gray-100 px-1 rounded">$1</code>').replace(/\n/g,"<br/>"):s,n=()=>{let s=null,r=!1,o;try{if(o=JSON.parse(i),o.ext){let t=o.ext.find(l=>l.type==="workflow_rewrite_checkpoint"||l.type==="debug_checkpoint"&&l.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(s=t.data.checkpoint_id,r=!0):(t=o.ext.find(l=>l.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(s=t.data.version_id,r=!0):(t=o.ext.find(l=>l.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(s=t.data.checkpoint_id,r=!1))),o.ext.find(l=>l.type==="workflow_update")&&(r=!0)}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const h=r?e.jsxs("div",{className:"flex items-center text-green-500",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"currentColor",viewBox:"0 0 20 20",children:e.jsx("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Successfully"})]}):e.jsxs("div",{className:"flex items-center text-[#fff]",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"28108",fill:"currentColor",children:e.jsx("path",{d:"M401.048025 844.855924c0 20.341281 16.643052 36.984333 36.984333 36.984333l147.936307 0c20.341281 0 36.984333-16.643052 36.984333-36.984333L622.952998 807.872614 401.048025 807.872614 401.048025 844.855924zM512 142.159744c-142.943596 0-258.888282 115.944686-258.888282 258.888282 0 88.021729 44.011376 165.503405 110.951975 212.288964l0 83.58365c0 20.341281 16.643052 36.984333 36.984333 36.984333l221.903949 0c20.341281 0 36.984333-16.643052 36.984333-36.984333l0-83.58365c66.941622-46.784536 110.951975-124.266212 110.951975-212.288964C770.888282 258.104429 654.943596 142.159744 512 142.159744zM617.588827 552.682561l-31.621185 22.005176 0 85.248569L438.031335 659.936307l0-85.063351L406.41015 552.866756c-49.743938-34.764781-79.33079-91.350544-79.33079-151.634536 0-101.890598 83.029018-184.92064 184.92064-184.92064s184.92064 83.029018 184.92064 184.92064C696.919617 461.332017 667.332764 517.91778 617.588827 552.682561z","p-id":"28277"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Finished"})]}),m=r?e.jsx("div",{className:"mt-3 text-xs text-gray-700",children:"💡 If you're not satisfied with the changes, click the restore button to revert to the previous version."}):null;return e.jsxs("div",{className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(v,{title:h,isWorkflowUpdate:r,className:"p-4",onFinishLoad:d,children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[c==="markdown"?e.jsx("div",{className:"text-[#fff]/70 text-sm leading-relaxed h-full",dangerouslySetInnerHTML:{__html:f(o?.text||"")}}):e.jsx("pre",{className:"whitespace-pre-wrap text-[#fff]/70 text-sm leading-relaxed h-full",children:o?.text||""}),m]})}),e.jsx("div",{className:"flex justify-end mt-2",children:!!s&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(k,{checkpointId:s,onRestore:()=>{console.log(`Workflow restored from ${r?"workflow update":"debug"} checkpoint`)},title:r?`Restore to this version (Version ${s})`:`Restore checkpoint ${s}`})})})]})};return e.jsx(g,{name:a,children:n()})}export{y as DebugResult};
@@ -1,7 +1,7 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as ue}from"./vendor-markdown-BAJQc3l1.js";import{W as Si,h as Ba,a as Bt}from"./message-components-Cf5ztmsp.js";import{g as _u,R as Oe,b as Wa,r as g,d as Ca,a as Ou}from"./vendor-react-Dixhfmvb.js";var ns={exports:{}};/*!
import{j as ue}from"./vendor-markdown-BAJQc3l1.js";import{W as Si,h as Ba,a as Bt}from"./message-components-DPTrVTCp.js";import{g as _u,R as Oe,b as Wa,r as g,d as Ca,a as Ou}from"./vendor-react-Dixhfmvb.js";var ns={exports:{}};/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
+3 -3
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9 -11
View File
@@ -30,7 +30,7 @@
"vis-network": "^9.1.9"
},
"devDependencies": {
"@comfyorg/comfyui-frontend-types": "^1.15.8",
"@comfyorg/comfyui-frontend-types": "^1.25.4",
"@tailwindcss/typography": "^0.5.15",
"@types/lodash": "^4.17.15",
"@types/node": "^22.10.0",
@@ -481,13 +481,12 @@
}
},
"node_modules/@comfyorg/comfyui-frontend-types": {
"version": "1.23.2",
"resolved": "https://registry.npmmirror.com/@comfyorg/comfyui-frontend-types/-/comfyui-frontend-types-1.23.2.tgz",
"integrity": "sha512-YQ046oFPovj7zpBRGGvYzQE3Tf+c4vXZWygnv/3SGcmVkk6nw2xFDmzMLZDn/8BdwVf66kEFHVzGXCxscpwkiA==",
"version": "1.25.4",
"resolved": "https://registry.npmmirror.com/@comfyorg/comfyui-frontend-types/-/comfyui-frontend-types-1.25.4.tgz",
"integrity": "sha512-kZ1ONmyXAc4bhyN+A3ym2qpKNSve996aHLv37wRB07de/HK0cdh9peBdVsoi4OvnDpQ0U3S3+48BqurZ2/+opA==",
"dev": true,
"license": "GPL-3.0-only",
"dependencies": {
"@comfyorg/litegraph": "^0.15.15"
"@comfyorg/litegraph": "^0.17.0"
},
"peerDependencies": {
"vue": "^3.5.13",
@@ -495,11 +494,10 @@
}
},
"node_modules/@comfyorg/litegraph": {
"version": "0.15.15",
"resolved": "https://registry.npmmirror.com/@comfyorg/litegraph/-/litegraph-0.15.15.tgz",
"integrity": "sha512-otOKgTxNPV6gEa6PW1fHGMMF8twjnZkP0vWQhGsRISK4vN8tPfX8O9sC9Hnq3nV8axaMv4/Ff49+7mMVcFEKeA==",
"dev": true,
"license": "MIT"
"version": "0.17.0",
"resolved": "https://registry.npmmirror.com/@comfyorg/litegraph/-/litegraph-0.17.0.tgz",
"integrity": "sha512-5Xu/G5NJFDucJontxXeXhzcaFuEXZznyg8u+joSjbe2G1991/tO7hYrPjXph2FLcovkSnEhG4ixdhyAwubGS3w==",
"dev": true
},
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
+1 -1
View File
@@ -36,7 +36,7 @@
"vis-network": "^9.1.9"
},
"devDependencies": {
"@comfyorg/comfyui-frontend-types": "^1.15.8",
"@comfyorg/comfyui-frontend-types": "^1.25.4",
"@tailwindcss/typography": "^0.5.15",
"@types/lodash": "^4.17.15",
"@types/node": "^22.10.0",
+16 -1
View File
@@ -2,7 +2,7 @@
* @Author: ai-business-hql ai.bussiness.hql@gmail.com
* @Date: 2025-02-17 20:53:45
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-06-19 14:57:20
* @LastEditTime: 2025-08-06 11:27:06
* @FilePath: /comfyui_copilot/ui/src/apis/comfyApiCustom.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -31,6 +31,21 @@ export async function getInstalledNodes() {
return Object.keys(objectInfos);
}
/**
* Get object info for a specific node class
* @param nodeClass The class name of the node to get info for
* @returns Promise with the node definition object
*/
export async function getObjectInfoByClass(nodeClass: string): Promise<any> {
try {
const response = await fetch(`/object_info/${nodeClass}`, { method: "GET" });
return await response.json();
} catch (error) {
console.error(`Error fetching object info for ${nodeClass}:`, error);
throw error;
}
}
export async function runPrompt(json_data: any): Promise<any> {
const response = await api.fetchApi("/prompt", {
method: "POST",
+57 -13
View File
@@ -2,7 +2,7 @@
* @Author: ai-business-hql qingli.hql@alibaba-inc.com
* @Date: 2025-06-24 16:29:05
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-08-02 11:33:35
* @LastEditTime: 2025-08-04 16:20:04
* @FilePath: /comfyui_copilot/ui/src/apis/workflowChatApi.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -135,7 +135,8 @@ export namespace WorkflowChatAPI {
ext: any | null = null,
trace_id?: string,
abortSignal?: AbortSignal,
historyMessages: Message[] = []
historyMessages: Message[] = [],
userMessageId?: string
): AsyncGenerator<ChatResponse> {
try {
const apiKey = getApiKey();
@@ -192,6 +193,7 @@ export namespace WorkflowChatAPI {
if (image.url) {
content.push({
type: 'input_image',
detail: 'low',
image_url:
image.url
});
@@ -207,16 +209,24 @@ export namespace WorkflowChatAPI {
// Handle ext parameter
let finalExt = ext ? (Array.isArray(ext) ? ext : [ext]) : [];
// Always get current workflow data for potential workflow modification requests
let currentWorkflowData: any = null;
try {
console.log('Getting current workflow data...');
const workflowPrompt = await app.graphToPrompt();
currentWorkflowData = workflowPrompt.output;
console.log('Successfully retrieved current workflow data');
} catch (error) {
console.error('Failed to get current workflow data:', error);
// Continue without workflow data - the backend will handle the error appropriately
// Save current workflow checkpoint before invoking if userMessageId is provided
let workflowCheckpointId: number | null = null;
if (userMessageId) {
try {
console.log('Saving workflow checkpoint before invoke...');
const workflowPrompt = await app.graphToPrompt();
const checkpointData = await saveWorkflowCheckpointBeforeInvoke(
sessionId,
workflowPrompt.output, // API format
workflowPrompt.workflow, // UI format
userMessageId
);
workflowCheckpointId = checkpointData.checkpoint_id;
console.log(`Successfully saved workflow checkpoint with ID: ${workflowCheckpointId}`);
} catch (error) {
console.error('Failed to save workflow checkpoint before invoke:', error);
// Continue without checkpoint - the backend will handle missing workflow data appropriately
}
}
// Prepare headers
@@ -267,7 +277,7 @@ export namespace WorkflowChatAPI {
ext: finalExt,
messages: allOpenaiMessages,
images: [], // 保持向后兼容,但现在图片已经在messages中
workflow_data: currentWorkflowData
workflow_checkpoint_id: workflowCheckpointId
}),
signal: controller.signal
});
@@ -785,6 +795,40 @@ export namespace WorkflowChatAPI {
throw error;
}
}
export async function saveWorkflowCheckpointBeforeInvoke(
sessionId: string,
workflowApi: any,
workflowUi: any,
messageId: string
): Promise<{ checkpoint_id: number; checkpoint_type: string; message_id: string }> {
try {
const response = await fetch('/api/save-workflow-checkpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'trace-id': generateUUID(),
},
body: JSON.stringify({
session_id: sessionId,
workflow_api: workflowApi,
workflow_ui: workflowUi,
checkpoint_type: 'user_message_checkpoint',
message_id: messageId
}),
});
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'Failed to save workflow checkpoint before invoke');
}
return result.data;
} catch (error) {
console.error('Error saving workflow checkpoint before invoke:', error);
throw error;
}
}
}
+69
View File
@@ -400,6 +400,75 @@ export function MessageList({ messages, latestInput, onOptionClick, installedNod
}
}
// 检查是否需要保存当前工作流的UI格式数据
// 如果ext数据中包含checkpoint_id且消息已完成,保存当前前端的workflow_ui
if (response.ext && message.finished) {
const checkpointExt = response.ext.find((item: ExtItem) =>
item.data && item.data.checkpoint_id &&
(item.type === 'debug_checkpoint' || item.type === 'workflow_rewrite_complete' || item.type === 'workflow_update_complete')
);
if (checkpointExt && checkpointExt.data?.checkpoint_id) {
const checkpointId = checkpointExt.data.checkpoint_id;
const saveWorkflowUIKey = `save_workflow_ui_${message.id}_${checkpointId}`;
if (!processedUpdates.current.has(saveWorkflowUIKey)) {
const saveWorkflowUIWithRetry = async (retryCount = 0) => {
try {
// 获取当前工作流的UI格式数据
if (typeof window !== 'undefined' && (window as any).app) {
const app = (window as any).app;
if (app.graph) {
const workflowPrompt = await app.graphToPrompt();
const workflowUI = workflowPrompt.workflow; // UI format
if (workflowUI) {
// 调用API更新workflow_ui字段
const response = await fetch('/api/update-workflow-ui', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
checkpoint_id: checkpointId,
workflow_data_ui: workflowUI
})
});
const result = await response.json();
if (result.success) {
console.log(`[MessageList] Successfully saved workflow UI for checkpoint ${checkpointId}`);
processedUpdates.current.add(saveWorkflowUIKey);
} else {
console.warn(`[MessageList] Failed to save workflow UI for checkpoint ${checkpointId}: ${result.message}`);
// 重试最多3次
if (retryCount < 2) {
setTimeout(() => {
saveWorkflowUIWithRetry(retryCount + 1);
}, 1000 * (retryCount + 1));
} else {
console.error(`[MessageList] Save workflow UI failed after 3 attempts for checkpoint ${checkpointId}`);
}
}
}
}
}
} catch (error) {
console.error(`[MessageList] Error saving workflow UI (attempt ${retryCount + 1}):`, error);
// 重试最多3次
if (retryCount < 2) {
setTimeout(() => {
saveWorkflowUIWithRetry(retryCount + 1);
}, 1000 * (retryCount + 1));
}
}
};
saveWorkflowUIWithRetry();
}
}
}
// 根据扩展类型添加对应组件
let ExtComponent = null;
if (workflowExt) {
+11 -1
View File
@@ -1,7 +1,16 @@
/*
* @Author: ai-business-hql qingli.hql@alibaba-inc.com
* @Date: 2025-07-30 16:30:22
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-08-06 11:30:42
* @FilePath: /comfyui_copilot/ui/src/components/ui/RestoreCheckpoint.tsx
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { Undo2 } from "lucide-react";
import { useState } from "react";
import { WorkflowChatAPI } from "../../apis/workflowChatApi";
import { app } from "../../utils/comfyapp";
import { loadApiWorkflowWithMissingNodes } from "../../utils/comfyuiWorkflowApi2Ui";
const RestoreCheckpoint = ({ checkpointId, onRestore, title }: { checkpointId: number; onRestore: () => void; title?: string }) => {
const [isRestoring, setIsRestoring] = useState(false);
@@ -23,7 +32,8 @@ const RestoreCheckpoint = ({ checkpointId, onRestore, title }: { checkpointId: n
app.loadGraphData(workflowToLoad);
} else {
// API format - use loadApiJson
app.loadApiJson(workflowToLoad);
// app.loadApiJson(workflowToLoad);
loadApiWorkflowWithMissingNodes(workflowToLoad);
}
console.log(`Restored workflow checkpoint ${checkpointId}`);
+1
View File
@@ -90,6 +90,7 @@ export interface Message {
trace_id?: string;
debugGuide?: boolean;
ext?: ExtItem[]; // 添加ext字段
workflow_checkpoint_id?: number; // 添加工作流检查点ID字段
}
export interface ToolCall {
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -2,7 +2,7 @@
* @Author: ai-business-hql qingli.hql@alibaba-inc.com
* @Date: 2025-02-17 20:53:45
* @LastEditors: ai-business-hql qingli.hql@alibaba-inc.com
* @LastEditTime: 2025-07-22 17:29:05
* @LastEditTime: 2025-08-06 11:51:11
* @FilePath: /comfyui_copilot/ui/src/utils/graphUtils.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -11,6 +11,7 @@
import LiteGraph from "../types/litegraph.d";
import { app } from "./comfyapp";
import { loadApiWorkflowWithMissingNodes } from "./comfyuiWorkflowApi2Ui";
export function addNodeOnGraph(type: string, options: any = {}) {
const node = LiteGraph.createNode(type, "", options);
@@ -53,7 +54,8 @@ export function applyNewWorkflow(workflow:any): boolean {
} else {
// api格式的工作流
console.log('[graphUtils] Loading API format workflow with node count:', Object.keys(workflow).length);
app.loadApiJson(workflow);
// app.loadApiJson(workflow);
loadApiWorkflowWithMissingNodes(workflow);
}
// 确保画布重新渲染
+8 -4
View File
@@ -384,8 +384,9 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
const traceId = generateUUID();
const userMessageId = generateUUID();
const userMessage: Message = {
id: generateUUID(),
id: userMessageId,
role: "user",
content: input,
trace_id: traceId,
@@ -424,7 +425,8 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
modelExt,
traceId,
abortControllerRef.current.signal,
state.messages
state.messages,
userMessageId // Pass the user message ID
)) {
const aiMessage: Message = {
id: aiMessageId,
@@ -473,8 +475,9 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
const traceId = generateUUID();
const userMessageId = generateUUID();
const userMessage: Message = {
id: generateUUID(),
id: userMessageId,
role: "user",
content: content,
trace_id: traceId,
@@ -501,7 +504,8 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
modelExt,
traceId,
abortControllerRef.current.signal,
state.messages
state.messages,
userMessageId // Pass the user message ID
)) {
const aiMessage: Message = {
id: aiMessageId,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB