feat: single-agent all-in-one
This commit is contained in:
@@ -5,10 +5,7 @@ import json
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional, TypedDict, List, Union
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
from sqlalchemy.orm import identity
|
||||
|
||||
from ..utils.globals import set_language
|
||||
from ..utils.auth_utils import extract_and_store_api_key
|
||||
@@ -20,18 +17,15 @@ import base64
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from ..service.debug_agent import debug_workflow_errors
|
||||
from ..service.debug_agent import debug_workflow_errors, debug_workflow_errors_single
|
||||
from ..dao.workflow_table import save_workflow_data, get_workflow_data_by_id, update_workflow_ui_by_id
|
||||
from ..service.mcp_client import comfyui_agent_invoke
|
||||
from ..service.all_in_one_agent import all_in_one_invoke
|
||||
from ..utils.request_context import set_request_context, get_session_id
|
||||
from ..utils.logger import log
|
||||
from ..utils.modelscope_gateway import ModelScopeGateway
|
||||
import folder_paths
|
||||
|
||||
# 全局下载进度存储
|
||||
download_progress = {}
|
||||
download_lock = threading.Lock()
|
||||
|
||||
# 不再使用内存存储会话消息,改为从前端传递历史消息
|
||||
|
||||
# 在文件开头添加
|
||||
@@ -69,98 +63,6 @@ class ChatResponse(TypedDict):
|
||||
format: str # 返回的格式
|
||||
ext: Optional[List[ExtItem]] # 扩展信息
|
||||
|
||||
# 下载进度回调类
|
||||
class DownloadProgressCallback:
|
||||
def __init__(self, id: str, filename: str, file_size: int, download_id: str):
|
||||
self.id = id
|
||||
self.filename = filename
|
||||
self.file_size = file_size
|
||||
self.download_id = download_id
|
||||
self.progress = 0
|
||||
self.status = "downloading" # downloading, completed, failed
|
||||
self.error_message = None
|
||||
self.start_time = time.time()
|
||||
|
||||
# 初始化进度记录
|
||||
with download_lock:
|
||||
download_progress[download_id] = {
|
||||
"id": id,
|
||||
"filename": filename,
|
||||
"file_size": file_size,
|
||||
"progress": 0,
|
||||
"percentage": 0.0,
|
||||
"status": "downloading",
|
||||
"start_time": self.start_time,
|
||||
"estimated_time": None,
|
||||
"speed": 0.0,
|
||||
"error_message": None
|
||||
}
|
||||
|
||||
def update(self, size: int):
|
||||
"""更新下载进度"""
|
||||
self.progress += size
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - self.start_time
|
||||
|
||||
# 计算下载速度和预估时间
|
||||
if elapsed_time > 0:
|
||||
speed = self.progress / elapsed_time # bytes per second
|
||||
if speed > 0 and self.progress < self.file_size:
|
||||
remaining_bytes = self.file_size - self.progress
|
||||
estimated_time = remaining_bytes / speed
|
||||
else:
|
||||
estimated_time = None
|
||||
else:
|
||||
speed = 0.0
|
||||
estimated_time = None
|
||||
|
||||
percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0
|
||||
|
||||
# 更新全局进度
|
||||
with download_lock:
|
||||
if self.download_id in download_progress:
|
||||
# 直接更新字典的值,而不是调用update方法
|
||||
progress_dict = download_progress[self.download_id]
|
||||
progress_dict["progress"] = self.progress
|
||||
progress_dict["percentage"] = round(percentage, 2)
|
||||
progress_dict["speed"] = round(speed, 2)
|
||||
progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None
|
||||
|
||||
def end(self, success: bool = True, error_message: str = None):
|
||||
"""下载结束回调"""
|
||||
current_time = time.time()
|
||||
total_time = current_time - self.start_time
|
||||
|
||||
if success:
|
||||
self.status = "completed"
|
||||
# 验证下载完整性
|
||||
assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}"
|
||||
else:
|
||||
self.status = "failed"
|
||||
self.error_message = error_message
|
||||
|
||||
# 更新最终状态
|
||||
with download_lock:
|
||||
if self.download_id in download_progress:
|
||||
# 直接更新字典的值,而不是调用update方法
|
||||
progress_dict = download_progress[self.download_id]
|
||||
progress_dict["status"] = self.status
|
||||
progress_dict["progress"] = self.file_size if success else self.progress
|
||||
if self.file_size > 0:
|
||||
progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100
|
||||
else:
|
||||
progress_dict["percentage"] = 0.0
|
||||
progress_dict["total_time"] = round(total_time, 2)
|
||||
progress_dict["error_message"] = self.error_message
|
||||
|
||||
def fail(self, error_message: str):
|
||||
"""下载失败回调"""
|
||||
self.end(success=False, error_message=error_message)
|
||||
|
||||
# 生成唯一下载ID
|
||||
def generate_download_id() -> str:
|
||||
import uuid
|
||||
return str(uuid.uuid4())
|
||||
|
||||
async def upload_to_oss(file_data: bytes, filename: str) -> str:
|
||||
# TODO: Implement your OSS upload logic here
|
||||
@@ -225,9 +127,7 @@ async def invoke_chat(request):
|
||||
await response.prepare(request)
|
||||
|
||||
session_id = req_json.get('session_id')
|
||||
prompt = req_json.get('prompt')
|
||||
images = req_json.get('images', [])
|
||||
intent = req_json.get('intent')
|
||||
ext = req_json.get('ext')
|
||||
historical_messages = req_json.get('messages', [])
|
||||
workflow_checkpoint_id = req_json.get('workflow_checkpoint_id')
|
||||
@@ -279,7 +179,6 @@ async def invoke_chat(request):
|
||||
accumulated_text = ""
|
||||
ext_data = None
|
||||
finished = True # Default to True
|
||||
has_sent_response = False
|
||||
previous_text_length = 0
|
||||
|
||||
log.info(f"config: {config}")
|
||||
@@ -353,6 +252,306 @@ async def invoke_chat(request):
|
||||
return response
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/api/chat-single/invoke")
|
||||
async def invoke_chat_single(request):
|
||||
log.info("Received invoke_chat_single request")
|
||||
|
||||
# Extract and store API key from Authorization header
|
||||
extract_and_store_api_key(request)
|
||||
|
||||
req_json = await request.json()
|
||||
log.info("Request JSON:", req_json)
|
||||
|
||||
response = web.StreamResponse(
|
||||
status=200,
|
||||
reason='OK',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Content-Type-Options': 'nosniff'
|
||||
}
|
||||
)
|
||||
await response.prepare(request)
|
||||
|
||||
session_id = req_json.get('session_id')
|
||||
images = req_json.get('images', [])
|
||||
ext = req_json.get('ext')
|
||||
historical_messages = req_json.get('messages', [])
|
||||
workflow_checkpoint_id = req_json.get('workflow_checkpoint_id')
|
||||
workflow_data = req_json.get('workflow_data') # Support debug-style payloads
|
||||
|
||||
# Language
|
||||
language = request.headers.get('Accept-Language', 'en')
|
||||
set_language(language)
|
||||
|
||||
# Config
|
||||
config = {
|
||||
"session_id": session_id,
|
||||
"workflow_checkpoint_id": workflow_checkpoint_id,
|
||||
"openai_api_key": request.headers.get('Openai-Api-Key'),
|
||||
"openai_base_url": request.headers.get('Openai-Base-Url'),
|
||||
# Optional Workflow LLM settings
|
||||
"workflow_llm_api_key": request.headers.get('Workflow-LLM-Api-Key'),
|
||||
"workflow_llm_base_url": request.headers.get('Workflow-LLM-Base-Url'),
|
||||
"workflow_llm_model": request.headers.get('Workflow-LLM-Model'),
|
||||
"model_select": next((x['data'][0] for x in ext if x['type'] == 'model_select' and x.get('data')), None) if ext else None
|
||||
}
|
||||
|
||||
# Set request context (isolation per session)
|
||||
set_request_context(session_id, workflow_checkpoint_id, config)
|
||||
|
||||
# If workflow_data provided (debug scenario), save it so tools like run_workflow/get_current_workflow can access
|
||||
try:
|
||||
if workflow_data:
|
||||
version_id = save_workflow_data(
|
||||
session_id=session_id,
|
||||
workflow_data=workflow_data,
|
||||
workflow_data_ui=None,
|
||||
attributes={
|
||||
"checkpoint_type": "debug_start_single",
|
||||
"description": "Initial workflow save for single-agent invoke",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
)
|
||||
log.info(f"[Single] Saved incoming workflow_data, version ID: {version_id}")
|
||||
except Exception as save_err:
|
||||
log.error(f"[Single] Failed to save workflow_data: {save_err}")
|
||||
|
||||
processed_images = []
|
||||
if images and len(images) > 0:
|
||||
log.info(f"Note: Received {len(images)} images in legacy format, but using OpenAI message format instead")
|
||||
|
||||
# Ensure messages exist and start with a user message (required by agents streaming)
|
||||
openai_messages = historical_messages if isinstance(historical_messages, list) else []
|
||||
needs_user_head = (len(openai_messages) == 0) or (openai_messages[0].get('role') != 'user')
|
||||
if needs_user_head:
|
||||
# If workflow_data present, default to a debug instruction; otherwise a generic chat starter
|
||||
default_user_text = "Validate and debug this ComfyUI workflow." if workflow_data else "Start the conversation."
|
||||
openai_messages = [{"role": "user", "content": default_user_text}] + openai_messages
|
||||
|
||||
try:
|
||||
accumulated_text = ""
|
||||
ext_data = None
|
||||
finished = True
|
||||
previous_text_length = 0
|
||||
|
||||
log.info(f"[Single] config: {config}")
|
||||
|
||||
async for result in all_in_one_invoke(openai_messages, None):
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
text, ext_with_finished = result
|
||||
if text:
|
||||
accumulated_text = text
|
||||
if ext_with_finished:
|
||||
ext_data = ext_with_finished.get("data")
|
||||
finished = ext_with_finished.get("finished", True)
|
||||
log.info(f"[Single] -- Received ext: {bool(ext_data)}, finished: {finished}")
|
||||
else:
|
||||
text_chunk = result
|
||||
if text_chunk:
|
||||
accumulated_text += text_chunk
|
||||
log.info(f"[Single] -- Received text chunk: '{text_chunk}', total length: {len(accumulated_text)}")
|
||||
|
||||
if accumulated_text and len(accumulated_text) > previous_text_length:
|
||||
chat_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=False,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=None
|
||||
)
|
||||
await response.write(json.dumps(chat_response).encode() + b"\n")
|
||||
previous_text_length = len(accumulated_text)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
final_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=finished,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=ext_data
|
||||
)
|
||||
await response.write(json.dumps(final_response).encode() + b"\n")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error in invoke_chat_single: {str(e)}")
|
||||
error_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=f"I apologize, but an error occurred: {str(e)}",
|
||||
finished=True,
|
||||
type="message",
|
||||
format="text",
|
||||
ext=None
|
||||
)
|
||||
await response.write(json.dumps(error_response).encode() + b"\n")
|
||||
|
||||
await response.write_eof()
|
||||
return response
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/api/debug-agent-single")
|
||||
async def invoke_debug_single(request):
|
||||
"""
|
||||
Single-agent Debug endpoint for analyzing ComfyUI workflow errors
|
||||
"""
|
||||
log.info("Received debug-agent-single request")
|
||||
|
||||
extract_and_store_api_key(request)
|
||||
req_json = await request.json()
|
||||
|
||||
response = web.StreamResponse(
|
||||
status=200,
|
||||
reason='OK',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Content-Type-Options': 'nosniff'
|
||||
}
|
||||
)
|
||||
await response.prepare(request)
|
||||
|
||||
session_id = req_json.get('session_id')
|
||||
workflow_data = req_json.get('workflow_data')
|
||||
|
||||
config = {
|
||||
"session_id": session_id,
|
||||
"model": "gemini-2.5-flash",
|
||||
"openai_api_key": request.headers.get('Openai-Api-Key'),
|
||||
"openai_base_url": request.headers.get('Openai-Base-Url'),
|
||||
"workflow_llm_api_key": request.headers.get('Workflow-LLM-Api-Key'),
|
||||
"workflow_llm_base_url": request.headers.get('Workflow-LLM-Base-Url'),
|
||||
"workflow_llm_model": request.headers.get('Workflow-LLM-Model'),
|
||||
}
|
||||
|
||||
language = request.headers.get('Accept-Language', 'en')
|
||||
set_language(language)
|
||||
|
||||
set_request_context(session_id, None, config)
|
||||
|
||||
log.info(f"Single Debug agent config: {config}")
|
||||
log.info(f"Session ID: {session_id}")
|
||||
log.info(f"Workflow nodes: {list(workflow_data.keys()) if workflow_data else 'None'}")
|
||||
|
||||
try:
|
||||
accumulated_text = ""
|
||||
final_ext_data = None
|
||||
finished = False
|
||||
|
||||
async for result in debug_workflow_errors_single(workflow_data):
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
text, ext = result
|
||||
if text:
|
||||
accumulated_text = text
|
||||
if ext:
|
||||
if isinstance(ext, dict) and "data" in ext and "finished" in ext:
|
||||
final_ext_data = ext["data"]
|
||||
finished = ext["finished"]
|
||||
|
||||
has_realtime_ext = False
|
||||
if final_ext_data:
|
||||
for ext_item in final_ext_data:
|
||||
if ext_item.get("type") in ["workflow_update", "param_update"]:
|
||||
has_realtime_ext = True
|
||||
break
|
||||
if has_realtime_ext or finished:
|
||||
chat_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=finished,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=final_ext_data
|
||||
)
|
||||
await response.write(json.dumps(chat_response).encode() + b"\n")
|
||||
elif not finished:
|
||||
chat_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=False,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=None
|
||||
)
|
||||
await response.write(json.dumps(chat_response).encode() + b"\n")
|
||||
else:
|
||||
final_ext_data = ext
|
||||
finished = False
|
||||
chat_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=False,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=ext
|
||||
)
|
||||
await response.write(json.dumps(chat_response).encode() + b"\n")
|
||||
else:
|
||||
chat_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=False,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=None
|
||||
)
|
||||
await response.write(json.dumps(chat_response).encode() + b"\n")
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
final_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=accumulated_text,
|
||||
finished=True,
|
||||
type="message",
|
||||
format="markdown",
|
||||
ext=final_ext_data if final_ext_data else [{"type": "debug_complete", "data": {"status": "completed"}}]
|
||||
)
|
||||
|
||||
if workflow_data and accumulated_text:
|
||||
try:
|
||||
current_session_id = get_session_id()
|
||||
checkpoint_id = save_workflow_data(
|
||||
session_id=current_session_id,
|
||||
workflow_data=workflow_data,
|
||||
workflow_data_ui=None,
|
||||
attributes={
|
||||
"checkpoint_type": "debug_complete",
|
||||
"description": "Workflow state after single-agent debug completion",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
)
|
||||
if final_response["ext"]:
|
||||
final_response["ext"].append({
|
||||
"type": "debug_checkpoint",
|
||||
"data": {"checkpoint_id": checkpoint_id, "checkpoint_type": "debug_complete"}
|
||||
})
|
||||
else:
|
||||
final_response["ext"] = [{
|
||||
"type": "debug_checkpoint",
|
||||
"data": {"checkpoint_id": checkpoint_id, "checkpoint_type": "debug_complete"}
|
||||
}]
|
||||
log.info(f"Single Debug completion checkpoint saved with ID: {checkpoint_id}")
|
||||
except Exception as checkpoint_error:
|
||||
log.error(f"Failed to save single debug completion checkpoint: {checkpoint_error}")
|
||||
|
||||
await response.write(json.dumps(final_response).encode() + b"\n")
|
||||
log.info("Single Debug agent processing complete")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error in single debug agent: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
error_response = ChatResponse(
|
||||
session_id=session_id,
|
||||
text=f"❌ Single Debug agent error: {str(e)}",
|
||||
finished=True,
|
||||
type="message",
|
||||
format="text",
|
||||
ext=[{"type": "error", "data": {"error": str(e)}}]
|
||||
)
|
||||
await response.write(json.dumps(error_response).encode() + b"\n")
|
||||
|
||||
await response.write_eof()
|
||||
return response
|
||||
@server.PromptServer.instance.routes.post("/api/save-workflow-checkpoint")
|
||||
async def save_workflow_checkpoint(request):
|
||||
"""
|
||||
@@ -723,354 +922,4 @@ async def update_workflow_ui(request):
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Failed to update workflow UI data: {str(e)}"
|
||||
})
|
||||
|
||||
@server.PromptServer.instance.routes.post("/api/download-model")
|
||||
async def download_model(request):
|
||||
"""
|
||||
Download model from ModelScope using SDK
|
||||
"""
|
||||
log.info("Received download-model request")
|
||||
req_json = await request.json()
|
||||
|
||||
try:
|
||||
id = req_json.get('id')
|
||||
model_id = req_json.get('model_id')
|
||||
model_type = req_json.get('model_type')
|
||||
dest_dir = req_json.get('dest_dir')
|
||||
|
||||
# 验证必需参数
|
||||
if not id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: id"
|
||||
})
|
||||
|
||||
if not model_id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: model_id"
|
||||
})
|
||||
|
||||
if not model_type:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: model_type"
|
||||
})
|
||||
|
||||
log.info(f"Downloading model: {model_id} (type: {model_type})")
|
||||
|
||||
# 生成下载ID
|
||||
download_id = generate_download_id()
|
||||
|
||||
# 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型
|
||||
resolved_dest_dir = None
|
||||
if dest_dir:
|
||||
resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}"))
|
||||
else:
|
||||
try:
|
||||
model_type_paths = folder_paths.get_folder_paths(model_type)
|
||||
resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir, model_type)
|
||||
except Exception:
|
||||
resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type)
|
||||
|
||||
# 创建进度回调
|
||||
progress_callback = DownloadProgressCallback(
|
||||
id=id,
|
||||
filename=f"{model_id}.{model_type}",
|
||||
file_size=0, # 实际大小会在下载过程中获取
|
||||
download_id=download_id
|
||||
)
|
||||
|
||||
# 启动下载任务(异步执行)
|
||||
async def download_task():
|
||||
try:
|
||||
# 调用下载方法 - 使用snapshot_download
|
||||
from modelscope import snapshot_download
|
||||
|
||||
# 创建进度回调包装器 - 实现ModelScope期望的接口
|
||||
class ProgressWrapper:
|
||||
"""Factory that returns a per-file progress object with update/end."""
|
||||
def __init__(self, download_id: str):
|
||||
self.download_id = download_id
|
||||
|
||||
def __call__(self, file_name: str, file_size: int):
|
||||
# Create a per-file progress tracker expected by ModelScope
|
||||
download_id = self.download_id
|
||||
|
||||
class _PerFileProgress:
|
||||
def __init__(self, fname: str, fsize: int):
|
||||
self.file_name = fname
|
||||
self.file_size = max(int(fsize or 0), 0)
|
||||
self.progress = 0
|
||||
self.last_update_time = time.time()
|
||||
self.last_downloaded = 0
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
# If unknown size, keep 0 to avoid div-by-zero
|
||||
download_progress[download_id]["file_size"] = self.file_size
|
||||
|
||||
def update(self, size: int):
|
||||
try:
|
||||
self.progress += int(size or 0)
|
||||
now = time.time()
|
||||
# Update global progress
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
dp = download_progress[download_id]
|
||||
dp["progress"] = self.progress
|
||||
if self.file_size > 0:
|
||||
dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2)
|
||||
# speed
|
||||
elapsed = max(now - self.last_update_time, 1e-6)
|
||||
speed = (self.progress - self.last_downloaded) / elapsed
|
||||
dp["speed"] = round(speed, 2)
|
||||
self.last_update_time = now
|
||||
self.last_downloaded = self.progress
|
||||
except Exception as e:
|
||||
log.error(f"Error in progress update: {e}")
|
||||
|
||||
def end(self):
|
||||
# Called by modelscope when a file finishes
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
dp = download_progress[download_id]
|
||||
if self.file_size > 0:
|
||||
dp["progress"] = self.file_size
|
||||
dp["percentage"] = 100.0
|
||||
|
||||
return _PerFileProgress(file_name, file_size)
|
||||
|
||||
progress_wrapper = ProgressWrapper(download_id)
|
||||
|
||||
# 添加调试日志
|
||||
log.info(f"Starting download with progress wrapper: {download_id}")
|
||||
|
||||
# 在线程中执行阻塞的下载,避免阻塞事件循环
|
||||
from functools import partial
|
||||
local_dir = await asyncio.to_thread(
|
||||
partial(
|
||||
snapshot_download,
|
||||
model_id=model_id,
|
||||
cache_dir=resolved_dest_dir,
|
||||
progress_callbacks=[progress_wrapper]
|
||||
)
|
||||
)
|
||||
|
||||
# 下载完成
|
||||
progress_callback.end(success=True)
|
||||
log.info(f"Model downloaded successfully to: {local_dir}")
|
||||
|
||||
# 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir)
|
||||
try:
|
||||
moved_count = 0
|
||||
allowed_exts = {
|
||||
".safetensors", ".ckpt", ".pt", ".pth", ".bin"
|
||||
# ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx"
|
||||
}
|
||||
for root, dirs, files in os.walk(local_dir):
|
||||
for name in files:
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
if ext in allowed_exts:
|
||||
src_path = os.path.join(root, name)
|
||||
target_dir = resolved_dest_dir
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
target_path = os.path.join(target_dir, name)
|
||||
# 如果已经在目标目录则跳过
|
||||
if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir):
|
||||
continue
|
||||
# 处理重名情况:自动追加 _1, _2 ...
|
||||
if os.path.exists(target_path):
|
||||
base, ext_real = os.path.splitext(name)
|
||||
idx = 1
|
||||
while True:
|
||||
candidate = f"{base}_{idx}{ext_real}"
|
||||
candidate_path = os.path.join(target_dir, candidate)
|
||||
if not os.path.exists(candidate_path):
|
||||
target_path = candidate_path
|
||||
break
|
||||
idx += 1
|
||||
shutil.move(src_path, target_path)
|
||||
moved_count += 1
|
||||
log.info(f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}")
|
||||
except Exception as move_err:
|
||||
log.error(f"Post-download move failed: {move_err}")
|
||||
|
||||
except Exception as e:
|
||||
progress_callback.fail(str(e))
|
||||
log.error(f"Download failed: {str(e)}")
|
||||
|
||||
# 启动异步下载任务
|
||||
asyncio.create_task(download_task())
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"download_id": download_id,
|
||||
"id": id,
|
||||
"model_id": model_id,
|
||||
"model_type": model_type,
|
||||
"dest_dir": resolved_dest_dir,
|
||||
"status": "started"
|
||||
},
|
||||
"message": f"Download started for model '{model_id}'"
|
||||
})
|
||||
|
||||
except ImportError as e:
|
||||
log.error(f"ModelScope SDK not installed: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error starting download: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Failed to start download: {str(e)}"
|
||||
})
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}")
|
||||
async def get_download_progress(request):
|
||||
"""
|
||||
Get download progress by download ID
|
||||
"""
|
||||
download_id = request.match_info.get('download_id')
|
||||
|
||||
if not download_id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing download_id parameter"
|
||||
})
|
||||
|
||||
with download_lock:
|
||||
progress_info = download_progress.get(download_id)
|
||||
|
||||
if not progress_info:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Download ID {download_id} not found"
|
||||
})
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": progress_info,
|
||||
"message": "Download progress retrieved successfully"
|
||||
})
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/download-progress")
|
||||
async def list_downloads(request):
|
||||
"""
|
||||
List all active downloads
|
||||
"""
|
||||
with download_lock:
|
||||
downloads = list(download_progress.keys())
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"downloads": downloads,
|
||||
"count": len(downloads)
|
||||
},
|
||||
"message": "Download list retrieved successfully"
|
||||
})
|
||||
|
||||
@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}")
|
||||
async def clear_download_progress(request):
|
||||
"""
|
||||
Clear download progress record (for cleanup)
|
||||
"""
|
||||
download_id = request.match_info.get('download_id')
|
||||
|
||||
if not download_id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing download_id parameter"
|
||||
})
|
||||
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
del download_progress[download_id]
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": f"Download progress {download_id} cleared successfully"
|
||||
})
|
||||
else:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Download ID {download_id} not found"
|
||||
})
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/model-searchs")
|
||||
async def model_suggests(request):
|
||||
"""
|
||||
Get model search list by keyword
|
||||
"""
|
||||
log.info("Received model-search request")
|
||||
try:
|
||||
keyword = request.query.get('keyword')
|
||||
|
||||
if not keyword:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: keyword"
|
||||
})
|
||||
|
||||
# 创建ModelScope网关实例
|
||||
gateway = ModelScopeGateway()
|
||||
|
||||
suggests = gateway.search(name=keyword)
|
||||
|
||||
list = suggests["data"] if suggests.get("data") else []
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"searchs": list,
|
||||
"total": len(list)
|
||||
},
|
||||
"message": f"Get searchs successfully"
|
||||
})
|
||||
|
||||
except ImportError as e:
|
||||
log.error(f"ModelScope SDK not installed: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error get model searchs: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Get model searchs failed: {str(e)}"
|
||||
})
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/model-paths")
|
||||
async def model_paths(request):
|
||||
"""
|
||||
Get model paths by type
|
||||
"""
|
||||
log.info("Received model-paths request")
|
||||
try:
|
||||
model_paths = list(folder_paths.folder_names_and_paths.keys())
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"paths": model_paths,
|
||||
},
|
||||
"message": f"Get paths successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error get model path: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Get model failed: {str(e)}"
|
||||
})
|
||||
@@ -0,0 +1,468 @@
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
import server
|
||||
from aiohttp import web
|
||||
import os
|
||||
import shutil
|
||||
from ..utils.logger import log
|
||||
from ..utils.modelscope_gateway import ModelScopeGateway
|
||||
import folder_paths
|
||||
|
||||
|
||||
# 全局下载进度存储
|
||||
download_progress = {}
|
||||
download_lock = threading.Lock()
|
||||
|
||||
# 下载进度回调类
|
||||
class DownloadProgressCallback:
|
||||
def __init__(self, id: str, filename: str, file_size: int, download_id: str):
|
||||
self.id = id
|
||||
self.filename = filename
|
||||
self.file_size = file_size
|
||||
self.download_id = download_id
|
||||
self.progress = 0
|
||||
self.status = "downloading" # downloading, completed, failed
|
||||
self.error_message = None
|
||||
self.start_time = time.time()
|
||||
|
||||
# 初始化进度记录
|
||||
with download_lock:
|
||||
download_progress[download_id] = {
|
||||
"id": id,
|
||||
"filename": filename,
|
||||
"file_size": file_size,
|
||||
"progress": 0,
|
||||
"percentage": 0.0,
|
||||
"status": "downloading",
|
||||
"start_time": self.start_time,
|
||||
"estimated_time": None,
|
||||
"speed": 0.0,
|
||||
"error_message": None
|
||||
}
|
||||
|
||||
def update(self, size: int):
|
||||
"""更新下载进度"""
|
||||
self.progress += size
|
||||
current_time = time.time()
|
||||
elapsed_time = current_time - self.start_time
|
||||
|
||||
# 计算下载速度和预估时间
|
||||
if elapsed_time > 0:
|
||||
speed = self.progress / elapsed_time # bytes per second
|
||||
if speed > 0 and self.progress < self.file_size:
|
||||
remaining_bytes = self.file_size - self.progress
|
||||
estimated_time = remaining_bytes / speed
|
||||
else:
|
||||
estimated_time = None
|
||||
else:
|
||||
speed = 0.0
|
||||
estimated_time = None
|
||||
|
||||
percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0
|
||||
|
||||
# 更新全局进度
|
||||
with download_lock:
|
||||
if self.download_id in download_progress:
|
||||
# 直接更新字典的值,而不是调用update方法
|
||||
progress_dict = download_progress[self.download_id]
|
||||
progress_dict["progress"] = self.progress
|
||||
progress_dict["percentage"] = round(percentage, 2)
|
||||
progress_dict["speed"] = round(speed, 2)
|
||||
progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None
|
||||
|
||||
def end(self, success: bool = True, error_message: str = None):
|
||||
"""下载结束回调"""
|
||||
current_time = time.time()
|
||||
total_time = current_time - self.start_time
|
||||
|
||||
if success:
|
||||
self.status = "completed"
|
||||
# 验证下载完整性
|
||||
assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}"
|
||||
else:
|
||||
self.status = "failed"
|
||||
self.error_message = error_message
|
||||
|
||||
# 更新最终状态
|
||||
with download_lock:
|
||||
if self.download_id in download_progress:
|
||||
# 直接更新字典的值,而不是调用update方法
|
||||
progress_dict = download_progress[self.download_id]
|
||||
progress_dict["status"] = self.status
|
||||
progress_dict["progress"] = self.file_size if success else self.progress
|
||||
if self.file_size > 0:
|
||||
progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100
|
||||
else:
|
||||
progress_dict["percentage"] = 0.0
|
||||
progress_dict["total_time"] = round(total_time, 2)
|
||||
progress_dict["error_message"] = self.error_message
|
||||
|
||||
def fail(self, error_message: str):
|
||||
"""下载失败回调"""
|
||||
self.end(success=False, error_message=error_message)
|
||||
|
||||
|
||||
# 生成唯一下载ID
|
||||
def generate_download_id() -> str:
|
||||
import uuid
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/api/download-model")
|
||||
async def download_model(request):
|
||||
"""
|
||||
Download model from ModelScope using SDK
|
||||
"""
|
||||
log.info("Received download-model request")
|
||||
req_json = await request.json()
|
||||
|
||||
try:
|
||||
id = req_json.get('id')
|
||||
model_id = req_json.get('model_id')
|
||||
model_type = req_json.get('model_type')
|
||||
dest_dir = req_json.get('dest_dir')
|
||||
|
||||
# 验证必需参数
|
||||
if not id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: id"
|
||||
})
|
||||
|
||||
if not model_id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: model_id"
|
||||
})
|
||||
|
||||
if not model_type:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: model_type"
|
||||
})
|
||||
|
||||
log.info(f"Downloading model: {model_id} (type: {model_type})")
|
||||
|
||||
# 生成下载ID
|
||||
download_id = generate_download_id()
|
||||
|
||||
# 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型
|
||||
resolved_dest_dir = None
|
||||
if dest_dir:
|
||||
resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}"))
|
||||
else:
|
||||
try:
|
||||
model_type_paths = folder_paths.get_folder_paths(model_type)
|
||||
resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir,
|
||||
model_type)
|
||||
except Exception:
|
||||
resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type)
|
||||
|
||||
# 创建进度回调
|
||||
progress_callback = DownloadProgressCallback(
|
||||
id=id,
|
||||
filename=f"{model_id}.{model_type}",
|
||||
file_size=0, # 实际大小会在下载过程中获取
|
||||
download_id=download_id
|
||||
)
|
||||
|
||||
# 启动下载任务(异步执行)
|
||||
async def download_task():
|
||||
try:
|
||||
# 调用下载方法 - 使用snapshot_download
|
||||
from modelscope import snapshot_download
|
||||
|
||||
# 创建进度回调包装器 - 实现ModelScope期望的接口
|
||||
class ProgressWrapper:
|
||||
"""Factory that returns a per-file progress object with update/end."""
|
||||
|
||||
def __init__(self, download_id: str):
|
||||
self.download_id = download_id
|
||||
|
||||
def __call__(self, file_name: str, file_size: int):
|
||||
# Create a per-file progress tracker expected by ModelScope
|
||||
download_id = self.download_id
|
||||
|
||||
class _PerFileProgress:
|
||||
def __init__(self, fname: str, fsize: int):
|
||||
self.file_name = fname
|
||||
self.file_size = max(int(fsize or 0), 0)
|
||||
self.progress = 0
|
||||
self.last_update_time = time.time()
|
||||
self.last_downloaded = 0
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
# If unknown size, keep 0 to avoid div-by-zero
|
||||
download_progress[download_id]["file_size"] = self.file_size
|
||||
|
||||
def update(self, size: int):
|
||||
try:
|
||||
self.progress += int(size or 0)
|
||||
now = time.time()
|
||||
# Update global progress
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
dp = download_progress[download_id]
|
||||
dp["progress"] = self.progress
|
||||
if self.file_size > 0:
|
||||
dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2)
|
||||
# speed
|
||||
elapsed = max(now - self.last_update_time, 1e-6)
|
||||
speed = (self.progress - self.last_downloaded) / elapsed
|
||||
dp["speed"] = round(speed, 2)
|
||||
self.last_update_time = now
|
||||
self.last_downloaded = self.progress
|
||||
except Exception as e:
|
||||
log.error(f"Error in progress update: {e}")
|
||||
|
||||
def end(self):
|
||||
# Called by modelscope when a file finishes
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
dp = download_progress[download_id]
|
||||
if self.file_size > 0:
|
||||
dp["progress"] = self.file_size
|
||||
dp["percentage"] = 100.0
|
||||
|
||||
return _PerFileProgress(file_name, file_size)
|
||||
|
||||
progress_wrapper = ProgressWrapper(download_id)
|
||||
|
||||
# 添加调试日志
|
||||
log.info(f"Starting download with progress wrapper: {download_id}")
|
||||
|
||||
# 在线程中执行阻塞的下载,避免阻塞事件循环
|
||||
from functools import partial
|
||||
local_dir = await asyncio.to_thread(
|
||||
partial(
|
||||
snapshot_download,
|
||||
model_id=model_id,
|
||||
cache_dir=resolved_dest_dir,
|
||||
progress_callbacks=[progress_wrapper]
|
||||
)
|
||||
)
|
||||
|
||||
# 下载完成
|
||||
progress_callback.end(success=True)
|
||||
log.info(f"Model downloaded successfully to: {local_dir}")
|
||||
|
||||
# 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir)
|
||||
try:
|
||||
moved_count = 0
|
||||
allowed_exts = {
|
||||
".safetensors", ".ckpt", ".pt", ".pth", ".bin"
|
||||
# ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx"
|
||||
}
|
||||
for root, dirs, files in os.walk(local_dir):
|
||||
for name in files:
|
||||
ext = os.path.splitext(name)[1].lower()
|
||||
if ext in allowed_exts:
|
||||
src_path = os.path.join(root, name)
|
||||
target_dir = resolved_dest_dir
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
target_path = os.path.join(target_dir, name)
|
||||
# 如果已经在目标目录则跳过
|
||||
if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir):
|
||||
continue
|
||||
# 处理重名情况:自动追加 _1, _2 ...
|
||||
if os.path.exists(target_path):
|
||||
base, ext_real = os.path.splitext(name)
|
||||
idx = 1
|
||||
while True:
|
||||
candidate = f"{base}_{idx}{ext_real}"
|
||||
candidate_path = os.path.join(target_dir, candidate)
|
||||
if not os.path.exists(candidate_path):
|
||||
target_path = candidate_path
|
||||
break
|
||||
idx += 1
|
||||
shutil.move(src_path, target_path)
|
||||
moved_count += 1
|
||||
log.info(
|
||||
f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}")
|
||||
except Exception as move_err:
|
||||
log.error(f"Post-download move failed: {move_err}")
|
||||
|
||||
except Exception as e:
|
||||
progress_callback.fail(str(e))
|
||||
log.error(f"Download failed: {str(e)}")
|
||||
|
||||
# 启动异步下载任务
|
||||
asyncio.create_task(download_task())
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"download_id": download_id,
|
||||
"id": id,
|
||||
"model_id": model_id,
|
||||
"model_type": model_type,
|
||||
"dest_dir": resolved_dest_dir,
|
||||
"status": "started"
|
||||
},
|
||||
"message": f"Download started for model '{model_id}'"
|
||||
})
|
||||
|
||||
except ImportError as e:
|
||||
log.error(f"ModelScope SDK not installed: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error starting download: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Failed to start download: {str(e)}"
|
||||
})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}")
|
||||
async def get_download_progress(request):
|
||||
"""
|
||||
Get download progress by download ID
|
||||
"""
|
||||
download_id = request.match_info.get('download_id')
|
||||
|
||||
if not download_id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing download_id parameter"
|
||||
})
|
||||
|
||||
with download_lock:
|
||||
progress_info = download_progress.get(download_id)
|
||||
|
||||
if not progress_info:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Download ID {download_id} not found"
|
||||
})
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": progress_info,
|
||||
"message": "Download progress retrieved successfully"
|
||||
})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/download-progress")
|
||||
async def list_downloads(request):
|
||||
"""
|
||||
List all active downloads
|
||||
"""
|
||||
with download_lock:
|
||||
downloads = list(download_progress.keys())
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"downloads": downloads,
|
||||
"count": len(downloads)
|
||||
},
|
||||
"message": "Download list retrieved successfully"
|
||||
})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}")
|
||||
async def clear_download_progress(request):
|
||||
"""
|
||||
Clear download progress record (for cleanup)
|
||||
"""
|
||||
download_id = request.match_info.get('download_id')
|
||||
|
||||
if not download_id:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing download_id parameter"
|
||||
})
|
||||
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
del download_progress[download_id]
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": f"Download progress {download_id} cleared successfully"
|
||||
})
|
||||
else:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Download ID {download_id} not found"
|
||||
})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/model-searchs")
|
||||
async def model_suggests(request):
|
||||
"""
|
||||
Get model search list by keyword
|
||||
"""
|
||||
log.info("Received model-search request")
|
||||
try:
|
||||
keyword = request.query.get('keyword')
|
||||
|
||||
if not keyword:
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "Missing required parameter: keyword"
|
||||
})
|
||||
|
||||
# 创建ModelScope网关实例
|
||||
gateway = ModelScopeGateway()
|
||||
|
||||
suggests = gateway.search(name=keyword)
|
||||
|
||||
list = suggests["data"] if suggests.get("data") else []
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"searchs": list,
|
||||
"total": len(list)
|
||||
},
|
||||
"message": f"Get searchs successfully"
|
||||
})
|
||||
|
||||
except ImportError as e:
|
||||
log.error(f"ModelScope SDK not installed: {str(e)}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error get model searchs: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Get model searchs failed: {str(e)}"
|
||||
})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/api/model-paths")
|
||||
async def model_paths(request):
|
||||
"""
|
||||
Get model paths by type
|
||||
"""
|
||||
log.info("Received model-paths request")
|
||||
try:
|
||||
model_paths = list(folder_paths.folder_names_and_paths.keys())
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"data": {
|
||||
"paths": model_paths,
|
||||
},
|
||||
"message": f"Get paths successfully"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error get model path: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"message": f"Get model failed: {str(e)}"
|
||||
})
|
||||
@@ -0,0 +1,254 @@
|
||||
'''
|
||||
All-in-one Single Agent for Chat + Debug + Rewrite (streaming)
|
||||
'''
|
||||
from typing import List, Dict, Any, Optional
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from ..utils.globals import BACKEND_BASE_URL, get_comfyui_copilot_api_key, WORKFLOW_MODEL_NAME
|
||||
from ..utils.logger import log
|
||||
from ..utils.request_context import get_session_id, get_config
|
||||
from ..utils.key_utils import workflow_config_adapt
|
||||
|
||||
# OpenAI Agents SDK imports
|
||||
try:
|
||||
from agents._config import set_default_openai_api
|
||||
from agents.items import ItemHelpers
|
||||
from agents.mcp import MCPServerSse
|
||||
from agents.run import Runner
|
||||
from agents.tracing import set_tracing_disabled
|
||||
except Exception:
|
||||
raise ImportError(
|
||||
"Detected incorrect or missing 'agents' package while loading All-in-one agent. "
|
||||
"Please install 'openai-agents'. Commands:\n"
|
||||
" python -m pip uninstall -y agents gym tensorflow\n"
|
||||
" python -m pip install -U openai-agents"
|
||||
)
|
||||
|
||||
from openai.types.responses import ResponseTextDeltaEvent
|
||||
|
||||
# Import tools from existing modules
|
||||
from .debug_agent import run_workflow, analyze_error_type, save_current_workflow
|
||||
from .link_agent_tools import analyze_missing_connections, apply_connection_fixes
|
||||
from .workflow_rewrite_tools import get_current_workflow, get_node_info, update_workflow, remove_node
|
||||
from .parameter_tools import (
|
||||
find_matching_parameter_value,
|
||||
get_model_files,
|
||||
suggest_model_download,
|
||||
update_workflow_parameter,
|
||||
)
|
||||
|
||||
from ..agent_factory import create_agent
|
||||
|
||||
|
||||
async def all_in_one_invoke(messages: List[Dict[str, Any]], images: Optional[List[Any]] = None):
|
||||
"""
|
||||
Invoke a single agent that can handle chat, debug, and rewrite in one place.
|
||||
|
||||
Yields tuples of (accumulated_text, {"data": ext_or_None, "finished": bool}).
|
||||
"""
|
||||
try:
|
||||
session_id = get_session_id()
|
||||
config = get_config()
|
||||
if not session_id:
|
||||
raise ValueError("No session_id found in request context")
|
||||
if not config:
|
||||
raise ValueError("No config found in request context")
|
||||
|
||||
# Adapt config if needed (keys, endpoints, etc.)
|
||||
config = workflow_config_adapt(config)
|
||||
|
||||
# Prepare MCP servers to expose external tools (recall_workflow, gen_workflow, searches, etc.)
|
||||
mcp_server = MCPServerSse(
|
||||
params={
|
||||
"url": BACKEND_BASE_URL + "/mcp-server/mcp",
|
||||
"timeout": 300.0,
|
||||
"headers": {"X-Session-Id": session_id, "Authorization": f"Bearer {get_comfyui_copilot_api_key()}"}
|
||||
},
|
||||
cache_tools_list=True,
|
||||
client_session_timeout_seconds=300.0
|
||||
)
|
||||
|
||||
bing_server = MCPServerSse(
|
||||
params={
|
||||
"url": "https://mcp.api-inference.modelscope.net/8c9fe550938e4f/sse",
|
||||
"timeout": 300.0,
|
||||
"headers": {"X-Session-Id": session_id, "Authorization": f"Bearer {get_comfyui_copilot_api_key()}"}
|
||||
},
|
||||
cache_tools_list=True,
|
||||
client_session_timeout_seconds=300.0
|
||||
)
|
||||
|
||||
async with mcp_server, bing_server:
|
||||
agent = create_agent(
|
||||
name="ComfyUI-AllInOne",
|
||||
model=WORKFLOW_MODEL_NAME,
|
||||
instructions=(
|
||||
"You are a single powerful assistant for ComfyUI.\n\n"
|
||||
"- Respond in the user's language.\n"
|
||||
"- For workflow generation requests, ALWAYS call recall_workflow first, then gen_workflow, and merge results.\n"
|
||||
"- For debug intents (workflow failed, node error): validate with run_workflow(), analyze with analyze_error_type(), then use link/parameter/rewrite tools accordingly and re-validate.\n"
|
||||
"- For rewrite intents (modify current canvas): use get_current_workflow(), get_node_info(), update_workflow(), remove_node(), and ensure connections/types are valid.\n"
|
||||
"- Prefer concise streaming updates.\n"
|
||||
),
|
||||
# Bind ALL local tools
|
||||
tools=[
|
||||
# Debug/validation
|
||||
run_workflow, analyze_error_type, save_current_workflow,
|
||||
# Link/connectivity
|
||||
analyze_missing_connections, apply_connection_fixes,
|
||||
# Rewrite/structure
|
||||
get_current_workflow, get_node_info, update_workflow, remove_node,
|
||||
# Parameters/models
|
||||
find_matching_parameter_value, get_model_files, suggest_model_download, update_workflow_parameter,
|
||||
],
|
||||
# Expose MCP tool servers for chat/search/workflow tools
|
||||
mcp_servers=[mcp_server, bing_server],
|
||||
config={
|
||||
"max_tokens": 8192,
|
||||
**config
|
||||
}
|
||||
)
|
||||
|
||||
# Stream the session
|
||||
agent_input = messages
|
||||
result = Runner.run_streamed(
|
||||
agent,
|
||||
input=agent_input,
|
||||
max_turns=30,
|
||||
)
|
||||
|
||||
current_text = ''
|
||||
last_yield_length = 0
|
||||
tool_results: Dict[str, Dict[str, Any]] = {}
|
||||
tool_call_queue: List[str] = []
|
||||
workflow_update_ext = None # collect workflow_update/param_update ext from tools
|
||||
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
||||
delta_text = event.data.delta
|
||||
if delta_text:
|
||||
current_text += delta_text
|
||||
if len(current_text) > last_yield_length:
|
||||
last_yield_length = len(current_text)
|
||||
yield (current_text, None)
|
||||
continue
|
||||
|
||||
if event.type == "run_item_stream_event":
|
||||
if event.item.type == "tool_call_item":
|
||||
tool_name = getattr(event.item.raw_item, 'name', 'unknown_tool')
|
||||
tool_call_queue.append(tool_name)
|
||||
log.info(f"[AllInOne] Tool called: {tool_name}")
|
||||
continue
|
||||
|
||||
if event.item.type == "tool_call_output_item":
|
||||
tool_output_data_str = str(event.item.output)
|
||||
# Associate with FIFO queue
|
||||
if tool_call_queue:
|
||||
tool_name = tool_call_queue.pop(0)
|
||||
else:
|
||||
tool_name = 'unknown_tool'
|
||||
|
||||
# Try to parse output as JSON and capture ext
|
||||
try:
|
||||
import json
|
||||
tool_output_data = json.loads(tool_output_data_str)
|
||||
if "ext" in tool_output_data and tool_output_data["ext"]:
|
||||
tool_ext_items = tool_output_data["ext"]
|
||||
# capture any workflow_update/param_update; keep entire list
|
||||
for ext_item in tool_ext_items:
|
||||
if ext_item.get("type") in ["workflow_update", "param_update"]:
|
||||
workflow_update_ext = tool_ext_items
|
||||
log.info(f"[AllInOne] Captured workflow-related ext from tool '{tool_name}'")
|
||||
break
|
||||
|
||||
if "text" in tool_output_data and tool_output_data.get('text'):
|
||||
parsed_output = json.loads(tool_output_data['text'])
|
||||
if isinstance(parsed_output, dict):
|
||||
answer = parsed_output.get("answer")
|
||||
data = parsed_output.get("data")
|
||||
tool_ext = parsed_output.get("ext")
|
||||
else:
|
||||
answer = None
|
||||
data = parsed_output if isinstance(parsed_output, list) else None
|
||||
tool_ext = None
|
||||
|
||||
tool_results[tool_name] = {
|
||||
"answer": answer,
|
||||
"data": data,
|
||||
"ext": tool_ext,
|
||||
"content_dict": parsed_output
|
||||
}
|
||||
except Exception:
|
||||
# Non-JSON output; store raw preview
|
||||
tool_results[tool_name] = {
|
||||
"answer": tool_output_data_str,
|
||||
"data": None,
|
||||
"ext": None,
|
||||
"content_dict": None
|
||||
}
|
||||
continue
|
||||
|
||||
# Post-stream processing: merge workflow tool results if present
|
||||
ext = None
|
||||
finished = True
|
||||
|
||||
workflow_tools_found = [t for t in ["recall_workflow", "gen_workflow"] if t in tool_results]
|
||||
if workflow_tools_found:
|
||||
if "recall_workflow" in tool_results and "gen_workflow" in tool_results:
|
||||
successful_workflows: List[Dict[str, Any]] = []
|
||||
recall_result = tool_results["recall_workflow"]
|
||||
gen_result = tool_results["gen_workflow"]
|
||||
if recall_result.get("data"):
|
||||
successful_workflows.extend(recall_result["data"])
|
||||
if gen_result.get("data"):
|
||||
# prioritize generated ones first
|
||||
successful_workflows = list(gen_result["data"]) + successful_workflows
|
||||
|
||||
# de-duplicate by id
|
||||
seen_ids = set()
|
||||
unique_workflows = []
|
||||
for wf in successful_workflows:
|
||||
wf_id = wf.get('id')
|
||||
if wf_id and wf_id not in seen_ids:
|
||||
seen_ids.add(wf_id)
|
||||
unique_workflows.append(wf)
|
||||
elif not wf_id:
|
||||
unique_workflows.append(wf)
|
||||
|
||||
if unique_workflows:
|
||||
ext = [{"type": "workflow", "data": unique_workflows}]
|
||||
elif "gen_workflow" in tool_results:
|
||||
gen_result = tool_results["gen_workflow"]
|
||||
if gen_result.get("data"):
|
||||
ext = [{"type": "workflow", "data": gen_result["data"]}]
|
||||
else:
|
||||
# only recall_workflow was called; keep unfinished to encourage gen_workflow next time
|
||||
ext = None
|
||||
finished = False
|
||||
|
||||
# Include any workflow_update/param_update ext captured during tool calls
|
||||
final_ext = ext
|
||||
if workflow_update_ext:
|
||||
if isinstance(workflow_update_ext, list):
|
||||
final_ext = workflow_update_ext + (ext if ext else [])
|
||||
else:
|
||||
final_ext = [workflow_update_ext] + (ext if ext else [])
|
||||
|
||||
ext_with_finished = {"data": final_ext, "finished": finished}
|
||||
yield (current_text, ext_with_finished)
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error in all_in_one_invoke: {str(e)}")
|
||||
log.error(f"Traceback: {traceback.format_exc()}")
|
||||
error_message = f"I apologize, but an error occurred while processing your request: {str(e)}"
|
||||
yield (error_message, {"data": None, "finished": True})
|
||||
|
||||
'''
|
||||
Author: ai-business-hql ai.bussiness.hql@gmail.com
|
||||
Date: 2025-10-29 16:51:18
|
||||
LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
|
||||
LastEditTime: 2025-10-29 17:01:17
|
||||
FilePath: /ComfyUI-Copilot/backend/service/all_in_one_agent.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
@@ -1,6 +1,8 @@
|
||||
'''
|
||||
Debug Agent for ComfyUI Workflow Error Analysis
|
||||
'''
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from ..utils.key_utils import workflow_config_adapt
|
||||
from ..agent_factory import create_agent
|
||||
from agents.items import ItemHelpers
|
||||
@@ -706,6 +708,200 @@ Start by validating the workflow to see its current state.""",
|
||||
yield (error_message, ext_with_finished)
|
||||
|
||||
|
||||
async def debug_workflow_errors_single(workflow_data: Dict[str, Any]):
|
||||
"""
|
||||
Analyze and debug workflow errors using a single-agent architecture.
|
||||
The single agent has access to all tools used across the multi-agent system
|
||||
and iteratively validates, analyzes, fixes, and re-validates until success or limits.
|
||||
|
||||
Args:
|
||||
workflow_data: Current workflow data from app.graphToPrompt()
|
||||
|
||||
Yields:
|
||||
tuple: (text, ext) where text is accumulated text and ext is structured data
|
||||
"""
|
||||
try:
|
||||
session_id = get_session_id()
|
||||
config = get_config()
|
||||
config = workflow_config_adapt(config)
|
||||
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
# Save initial workflow snapshot
|
||||
log.info(f"[SingleAgent] Saving workflow data for session {session_id}")
|
||||
save_result = save_workflow_data(
|
||||
session_id,
|
||||
workflow_data,
|
||||
attributes={"action": "debug_start_single", "description": "Initial workflow save for single-agent debugging"}
|
||||
)
|
||||
log.info(f"[SingleAgent] Workflow saved with version ID: {save_result}")
|
||||
|
||||
single_agent = create_agent(
|
||||
name="ComfyUI-Debug-SingleAgent",
|
||||
instructions=f"""
|
||||
You are a single ComfyUI workflow debugging expert with access to all specialized tools.
|
||||
|
||||
Your iterative loop (up to 10 cycles):
|
||||
1) Validate workflow using run_workflow()
|
||||
2) If errors: analyze with analyze_error_type() and decide fix strategy
|
||||
- Connection errors: use analyze_missing_connections() then apply_connection_fixes()
|
||||
- Parameter errors: use find_matching_parameter_value(), get_model_files(), update_workflow_parameter()
|
||||
• For model downloads required: use suggest_model_download() and STOP (final response)
|
||||
- Structural issues: use get_current_workflow(), get_node_info(), update_workflow()
|
||||
3) After any fix, immediately re-run run_workflow() to verify
|
||||
4) Repeat until validation succeeds or loop limit reached
|
||||
|
||||
Guidelines:
|
||||
- Be concise and stream updates during the process
|
||||
- Prefer high-confidence fixes first; add nodes only when necessary
|
||||
- Do not modify unrelated parameters
|
||||
- Language: {get_language()}
|
||||
""",
|
||||
model=WORKFLOW_MODEL_NAME,
|
||||
tools=[
|
||||
# Coordinator tools
|
||||
run_workflow, analyze_error_type, save_current_workflow,
|
||||
# Link/structure tools
|
||||
analyze_missing_connections, apply_connection_fixes, get_current_workflow, get_node_info, update_workflow,
|
||||
# Parameter tools
|
||||
find_matching_parameter_value, get_model_files, suggest_model_download, update_workflow_parameter,
|
||||
],
|
||||
config={
|
||||
"max_tokens": 8192,
|
||||
**config
|
||||
}
|
||||
)
|
||||
|
||||
messages = [{"role": "user", "content": "Validate and debug this ComfyUI workflow using the iterative loop until success."}]
|
||||
|
||||
log.info(f"[SingleAgent] Starting validation process for session {session_id}")
|
||||
result = Runner.run_streamed(
|
||||
single_agent,
|
||||
input=messages,
|
||||
max_turns=30,
|
||||
)
|
||||
|
||||
current_text = ''
|
||||
current_agent = "ComfyUI-Debug-SingleAgent"
|
||||
last_yielded_length = 0
|
||||
debug_events = []
|
||||
workflow_update_ext = None
|
||||
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
||||
delta_text = event.data.delta
|
||||
if delta_text:
|
||||
current_text += delta_text
|
||||
if len(current_text) > last_yielded_length:
|
||||
last_yielded_length = len(current_text)
|
||||
yield (current_text, None)
|
||||
elif event.type == "run_item_stream_event":
|
||||
item_updated = False
|
||||
if event.item.type == "tool_call_item":
|
||||
tool_name = getattr(event.item.raw_item, 'name', 'unknown_tool')
|
||||
log.info(f"[SingleAgent] -- Tool called: {tool_name}")
|
||||
current_text += f"\n\n⚙ *{current_agent} is using {tool_name}...*\n\n"
|
||||
item_updated = True
|
||||
debug_events.append({
|
||||
"type": "tool_call",
|
||||
"tool": tool_name,
|
||||
"agent": current_agent,
|
||||
"timestamp": len(current_text)
|
||||
})
|
||||
elif event.item.type == "tool_call_output_item":
|
||||
output = str(event.item.output)
|
||||
output_preview = output[:200] + "..." if len(output) > 200 else output
|
||||
current_text += f"\n\n● *Tool execution completed*\n\n```\n{output_preview}\n```\n\n"
|
||||
item_updated = True
|
||||
try:
|
||||
tool_output_json = json.loads(output)
|
||||
if "ext" in tool_output_json and tool_output_json["ext"]:
|
||||
for ext_item in tool_output_json["ext"]:
|
||||
if ext_item.get("type") == "workflow_update" or ext_item.get("type") == "param_update":
|
||||
workflow_update_ext = ext_item
|
||||
log.info(f"[SingleAgent] -- Captured {ext_item.get('type')} ext, yielding immediately")
|
||||
ext_with_finished = {"data": [ext_item], "finished": False}
|
||||
yield (current_text, ext_with_finished)
|
||||
break
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
debug_events.append({
|
||||
"type": "tool_result",
|
||||
"output_preview": output_preview,
|
||||
"agent": current_agent,
|
||||
"timestamp": len(current_text)
|
||||
})
|
||||
elif event.item.type == "message_output_item":
|
||||
try:
|
||||
message_content = ItemHelpers.text_message_output(event.item)
|
||||
if message_content and message_content.strip():
|
||||
if message_content not in current_text:
|
||||
current_text += f"\n\n{message_content}\n\n"
|
||||
item_updated = True
|
||||
debug_events.append({
|
||||
"type": "message_complete",
|
||||
"content_length": len(message_content),
|
||||
"agent": current_agent,
|
||||
"timestamp": len(current_text)
|
||||
})
|
||||
except Exception as e:
|
||||
log.error(f"[SingleAgent] Error processing message output: {str(e)}")
|
||||
if item_updated:
|
||||
last_yielded_length = len(current_text)
|
||||
yield (current_text, None)
|
||||
|
||||
log.info("[SingleAgent] === Debug process complete ===")
|
||||
|
||||
debug_completion_checkpoint_id = None
|
||||
try:
|
||||
current_workflow = get_workflow_data(session_id)
|
||||
if current_workflow:
|
||||
debug_completion_checkpoint_id = save_workflow_data(
|
||||
session_id,
|
||||
current_workflow,
|
||||
workflow_data_ui=None,
|
||||
attributes={
|
||||
"checkpoint_type": "debug_complete",
|
||||
"description": "Workflow state after single-agent debug completion",
|
||||
"action": "debug_complete_single",
|
||||
"final_agent": current_agent
|
||||
}
|
||||
)
|
||||
log.info(f"[SingleAgent] Debug completion checkpoint saved with ID: {debug_completion_checkpoint_id}")
|
||||
except Exception as checkpoint_error:
|
||||
log.error(f"[SingleAgent] Failed to save debug completion checkpoint: {checkpoint_error}")
|
||||
|
||||
debug_ext = [{
|
||||
"type": "debug_complete",
|
||||
"data": {
|
||||
"status": "completed",
|
||||
"final_agent": current_agent,
|
||||
"events": debug_events,
|
||||
"total_events": len(debug_events)
|
||||
}
|
||||
}]
|
||||
if debug_completion_checkpoint_id:
|
||||
debug_ext.append({
|
||||
"type": "debug_checkpoint",
|
||||
"data": {
|
||||
"checkpoint_id": debug_completion_checkpoint_id,
|
||||
"checkpoint_type": "debug_complete"
|
||||
}
|
||||
})
|
||||
final_ext = debug_ext
|
||||
if workflow_update_ext:
|
||||
final_ext = [workflow_update_ext] + debug_ext
|
||||
log.info("[SingleAgent] -- Including workflow_update ext in final response")
|
||||
ext_with_finished = {"data": final_ext, "finished": True}
|
||||
yield (current_text, ext_with_finished)
|
||||
except Exception as e:
|
||||
log.error(f"[SingleAgent] Error in debug_workflow_errors_single: {str(e)}")
|
||||
error_message = f"\n\n× Error occurred during single-agent debugging: {str(e)}\n\n"
|
||||
ext_with_finished = {"finished": True}
|
||||
yield (error_message, ext_with_finished)
|
||||
|
||||
|
||||
# Test function
|
||||
async def test_debug():
|
||||
"""Test the debug agent with a sample workflow"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
function getImportPath(filename) {
|
||||
return `./${filename}`;
|
||||
}
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-CTj3qusy.js","copilot_web/vendor-markdown-B4Av9P7l.js","copilot_web/vendor-react-ByKzdfMq.js","copilot_web/message-components-CzGdQ9TQ.js","copilot_web/input.js","copilot_web/assets/input-UcJyCCDl.css","copilot_web/fonts.css"].map(path => {
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-DCaYfVyA.js","copilot_web/vendor-markdown-B4Av9P7l.js","copilot_web/vendor-react-ByKzdfMq.js","copilot_web/message-components-CDlGtWLl.js","copilot_web/input.js","copilot_web/assets/input-UcJyCCDl.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 o}from"./vendor-markdown-B4Av9P7l.js";import{r as t,R as i}from"./vendor-react-ByKzdfMq.js";import{C as n}from"./message-components-CzGdQ9TQ.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-CTj3qusy.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function c(){const[e,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),o.jsx(n,{children:o.jsx("div",{className:"h-full w-full flex flex-col",children:o.jsx(t.Suspense,{fallback:o.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:o.jsx(d,{visible:!0,triggerUsage:e,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 o}from"./vendor-markdown-B4Av9P7l.js";import{r as t,R as i}from"./vendor-react-ByKzdfMq.js";import{C as n}from"./message-components-CDlGtWLl.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-DCaYfVyA.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function c(){const[e,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),o.jsx(n,{children:o.jsx("div",{className:"h-full w-full flex flex-col",children:o.jsx(t.Suspense,{fallback:o.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:o.jsx(d,{visible:!0,triggerUsage:e,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
-1
@@ -1,5 +1,5 @@
|
||||
function getImportPath(filename) {
|
||||
return `./${filename}`;
|
||||
}
|
||||
import{j as s}from"./vendor-markdown-B4Av9P7l.js";import{r as i}from"./vendor-react-ByKzdfMq.js";import{u as A,p as E,R as D,o as m,W as f,a as b}from"./message-components-CzGdQ9TQ.js";function P({content:h,name:k="Assistant",avatar:O,onAddMessage:y,onUpdateMessage:c,messageId:_}){const{dispatch:u,abortControllerRef:l}=A(),[d,w]=i.useState(!1),[T,G]=i.useState(""),[J,S]=i.useState(!1),[j,v]=i.useState(null),r=i.useMemo(()=>{try{return JSON.parse(h)}catch(e){return console.error("Failed to parse message content:",e),null}},[h]),x=i.useMemo(()=>{if(!r||!r.ext)return null;const e=r.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},[r])||j,N=async()=>{if(d)return;const e=m(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
|
||||
import{j as s}from"./vendor-markdown-B4Av9P7l.js";import{r as i}from"./vendor-react-ByKzdfMq.js";import{u as A,p as E,R as D,o as m,W as f,a as b}from"./message-components-CDlGtWLl.js";function P({content:h,name:k="Assistant",avatar:O,onAddMessage:y,onUpdateMessage:c,messageId:_}){const{dispatch:u,abortControllerRef:l}=A(),[d,w]=i.useState(!1),[T,G]=i.useState(""),[J,S]=i.useState(!1),[j,v]=i.useState(null),r=i.useMemo(()=>{try{return JSON.parse(h)}catch(e){return console.error("Failed to parse message content:",e),null}},[h]),x=i.useMemo(()=>{if(!r||!r.ext)return null;const e=r.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},[r])||j,N=async()=>{if(d)return;const e=m(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
|
||||
`,ext:[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};y?.(t);try{f.trackEvent({event_type:"debug_trigger",message_type:"debug",message_id:e,data:{}}),await C(),u({type:"SET_LOADING",payload:!0}),await I(e)}finally{w(!1)}},C=async()=>{try{const e=await b.graphToPrompt(),t=localStorage.getItem("sessionId")||"";if(t&&e){const n=await f.saveWorkflowCheckpoint(t,e.output,e.workflow,"debug_start");if(v(n.version_id),console.log(`Saved debug start checkpoint: ${n.version_id}`),c&&r){const o=[...r.ext||[]].filter(g=>g.type!=="debug_start_checkpoint"&&!(g.type==="debug_checkpoint"&&g.data?.checkpoint_type==="debug_start"));o.push({type:"debug_start_checkpoint",data:{checkpoint_id:n.version_id,checkpoint_type:"debug_start"}});const p={id:_||m(),role:"ai",content:JSON.stringify({text:r.text,ext:o}),format:"debug_guide",name:"Assistant"};c(p)}}}catch(e){console.error("Failed to save checkpoint before debug:",e)}},I=async e=>{try{const t=await b.graphToPrompt();let n="",a=null;l&&(l.current=new AbortController);for await(const o of f.streamDebugAgent(t,l?.current?.signal||void 0))if(o.text){n=o.text,o.ext&&(a=o.ext);const p={id:e,role:"ai",content:JSON.stringify({text:n,ext:a||[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};c?.(p)}c?.({id:e,role:"ai",content:JSON.stringify({text:n,ext:a||[]}),format:"markdown",name:"Assistant",finished:!0,debugGuide:!0}),u({type:"SET_LOADING",payload:!1}),l.current=null}catch(t){console.error("Error calling debug agent:",t),S(!1);const a={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?.(a),u({type:"SET_LOADING",payload:!1}),l.current=null}};return r?s.jsx(E,{name:k,children:s.jsxs("div",{className:"bg-gray-100 pt-4 px-4 pb-3 rounded-lg",children:[s.jsx("div",{className:"flex justify-between items-start",children:s.jsx("p",{className:"text-gray-700 text-sm flex-1",children:r.text})}),s.jsx("div",{className:"flex justify-end mt-4",children:s.jsx("button",{onClick:N,disabled:d,className:`debug-btn bg-debug-btn text-sm text-[#fff] rounded-lg px-4 py-2 ${d?"cursor-not-allowed":"cursor-pointer"}`,children:d?"Analyzing...":"Debug Errors"})}),s.jsx("div",{className:"flex justify-end mt-2",children:!!x&&s.jsx("div",{className:"ml-2 flex-shrink-0",children:s.jsx(D,{checkpointId:x,onRestore:()=>{console.log("Workflow restored from checkpoint")}})})})]})}):null}export{P as DebugGuide};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
function getImportPath(filename) {
|
||||
return `./${filename}`;
|
||||
}
|
||||
import{j as e}from"./vendor-markdown-B4Av9P7l.js";import{b as u,c as w,p as g,G as k,R as j}from"./message-components-CzGdQ9TQ.js";import{r as p}from"./vendor-react-ByKzdfMq.js";import{B as v,M as N}from"./workflowChat-CTj3qusy.js";import"./input.js";/* empty css */import"./App-fluNqnYs.js";const y=n=>{const{title:a="",isWorkflowUpdate:f=!1,className:d="",children:c}=n,[i,r]=p.useState(!1);return e.jsx(v,{className:"rounded-lg ",borderClassName:"rounded-lg",children:e.jsxs("div",{className:`flex flex-col ${d} ${i?"":"max-h-[200px]"}`,children:[e.jsxs("div",{className:"flex justify-between items-center pb-2 border-b border-gray-100",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:()=>{r(!i)},children:i?e.jsx(u,{className:"text-gray-700"}):e.jsx(w,{className:"text-gray-700"})})]}),e.jsx("div",{className:"overflow-hidden flex-1",children:c})]})})};function E({content:n,name:a="Assistant",avatar:f,format:d="markdown"}){const c=p.useRef(null),i=()=>{let r=null,l=!1,o,x=[];try{if(o=JSON.parse(n),o.ext){let t=o.ext.find(s=>s.type==="workflow_rewrite_checkpoint"||s.type==="debug_checkpoint"&&s.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(r=t.data.checkpoint_id,l=!0):(t=o.ext.find(s=>s.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(r=t.data.version_id,l=!0):(t=o.ext.find(s=>s.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(r=t.data.checkpoint_id,l=!1))),o.ext.find(s=>s.type==="workflow_update")&&(l=!0),x=o.ext.find(s=>s.type==="param_update")?.data?.model_suggest||[]}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const h=l?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-gray-900",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=l?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",{ref:c,className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(y,{title:h,isWorkflowUpdate:l,className:"p-4",children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[d==="markdown"?e.jsx(k,{response:o||{}}):e.jsx("pre",{className:"whitespace-pre-wrap text-gray-700 text-sm leading-relaxed h-full",children:o?.text||""}),m]})}),x?.length>0&&e.jsx(N,{modelList:x,showPagination:!1}),e.jsx("div",{className:"flex justify-end mt-2",children:!!r&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(j,{checkpointId:r,onRestore:()=>{console.log(`Workflow restored from ${l?"workflow update":"debug"} checkpoint`)},title:l?`Restore to this version (Version ${r})`:`Restore checkpoint ${r}`})})})]})};return e.jsx(g,{name:a,children:i()})}export{E as DebugResult};
|
||||
import{j as e}from"./vendor-markdown-B4Av9P7l.js";import{b as u,c as w,p as g,G as k,R as j}from"./message-components-CDlGtWLl.js";import{r as p}from"./vendor-react-ByKzdfMq.js";import{B as v,M as N}from"./workflowChat-DCaYfVyA.js";import"./input.js";/* empty css */import"./App-TDLVaCDl.js";const y=n=>{const{title:a="",isWorkflowUpdate:f=!1,className:d="",children:c}=n,[i,r]=p.useState(!1);return e.jsx(v,{className:"rounded-lg ",borderClassName:"rounded-lg",children:e.jsxs("div",{className:`flex flex-col ${d} ${i?"":"max-h-[200px]"}`,children:[e.jsxs("div",{className:"flex justify-between items-center pb-2 border-b border-gray-100",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:()=>{r(!i)},children:i?e.jsx(u,{className:"text-gray-700"}):e.jsx(w,{className:"text-gray-700"})})]}),e.jsx("div",{className:"overflow-hidden flex-1",children:c})]})})};function E({content:n,name:a="Assistant",avatar:f,format:d="markdown"}){const c=p.useRef(null),i=()=>{let r=null,l=!1,o,x=[];try{if(o=JSON.parse(n),o.ext){let t=o.ext.find(s=>s.type==="workflow_rewrite_checkpoint"||s.type==="debug_checkpoint"&&s.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(r=t.data.checkpoint_id,l=!0):(t=o.ext.find(s=>s.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(r=t.data.version_id,l=!0):(t=o.ext.find(s=>s.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(r=t.data.checkpoint_id,l=!1))),o.ext.find(s=>s.type==="workflow_update")&&(l=!0),x=o.ext.find(s=>s.type==="param_update")?.data?.model_suggest||[]}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const h=l?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-gray-900",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=l?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",{ref:c,className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(y,{title:h,isWorkflowUpdate:l,className:"p-4",children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[d==="markdown"?e.jsx(k,{response:o||{}}):e.jsx("pre",{className:"whitespace-pre-wrap text-gray-700 text-sm leading-relaxed h-full",children:o?.text||""}),m]})}),x?.length>0&&e.jsx(N,{modelList:x,showPagination:!1}),e.jsx("div",{className:"flex justify-end mt-2",children:!!r&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(j,{checkpointId:r,onRestore:()=>{console.log(`Workflow restored from ${l?"workflow update":"debug"} checkpoint`)},title:l?`Restore to this version (Version ${r})`:`Restore checkpoint ${r}`})})})]})};return e.jsx(g,{name:a,children:i()})}export{E as DebugResult};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
* @Author: ai-business-hql qingli.hql@alibaba-inc.com
|
||||
* @Date: 2025-06-24 16:29:05
|
||||
* @LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
|
||||
* @LastEditTime: 2025-10-15 14:49:15
|
||||
* @LastEditTime: 2025-10-29 15:35:51
|
||||
* @FilePath: /comfyui_copilot/ui/src/apis/workflowChatApi.ts
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
@@ -287,7 +287,8 @@ export namespace WorkflowChatAPI {
|
||||
});
|
||||
}
|
||||
|
||||
let chatUrl = `/api/chat/invoke`
|
||||
// let chatUrl = `/api/chat/invoke`
|
||||
let chatUrl = `/api/chat-single/invoke`
|
||||
if(intent && intent !== '') {
|
||||
chatUrl = `${BASE_URL}/api/chat/invoke`
|
||||
} else {
|
||||
@@ -707,7 +708,8 @@ export namespace WorkflowChatAPI {
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch('/api/debug-agent', {
|
||||
// const response = await fetch('/api/debug-agent-single', {
|
||||
const response = await fetch('/api/chat-single/invoke', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user