feat: stop feature

feat: model-manager - support background tasking
This commit is contained in:
Dr.Lt.Data 2025-02-01 16:35:56 +09:00
parent a3d6fcccb7
commit 510c364607
5 changed files with 345 additions and 158 deletions

View File

@ -41,7 +41,7 @@ import manager_downloader
from node_package import InstalledNodePackage from node_package import InstalledNodePackage
version_code = [3, 12, 2] version_code = [3, 13]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')

View File

@ -369,12 +369,14 @@ def nickname_filter(json_obj):
return json_obj return json_obj
install_queue = queue.Queue() task_queue = queue.Queue()
install_result = {} nodepack_result = {}
model_result = {}
async def install_worker(): async def task_worker():
global install_result global task_queue
global install_queue global nodepack_result
global model_result
async def do_install(item): async def do_install(item):
ui_id, node_spec_str, channel, mode, skip_post_install = item ui_id, node_spec_str, channel, mode, skip_post_install = item
@ -384,7 +386,7 @@ async def install_worker():
if node_spec is None: if node_spec is None:
logging.error(f"Cannot resolve install target: '{node_spec_str}'") logging.error(f"Cannot resolve install target: '{node_spec_str}'")
install_result[ui_id] = f"Cannot resolve install target: '{node_spec_str}'" nodepack_result[ui_id] = f"Cannot resolve install target: '{node_spec_str}'"
return return
node_name, version_spec, is_specified = node_spec node_name, version_spec, is_specified = node_spec
@ -393,18 +395,18 @@ async def install_worker():
if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']: if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}") logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}")
install_result[ui_id] = res.msg nodepack_result[ui_id] = res.msg
return return
elif not res.result: elif not res.result:
logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}") logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}")
install_result[ui_id] = res.msg nodepack_result[ui_id] = res.msg
return return
install_result[ui_id] = 'success' nodepack_result[ui_id] = 'success'
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
install_result[ui_id] = f"Installation failed:\n{node_spec_str}" nodepack_result[ui_id] = f"Installation failed:\n{node_spec_str}"
async def do_update(item): async def do_update(item):
ui_id, node_name, node_ver = item ui_id, node_name, node_ver = item
@ -415,14 +417,14 @@ async def install_worker():
manager_util.clear_pip_cache() manager_util.clear_pip_cache()
if res.result: if res.result:
install_result[ui_id] = 'success' nodepack_result[ui_id] = 'success'
return return
logging.error(f"\nERROR: An error occurred while updating '{node_name}'.") logging.error(f"\nERROR: An error occurred while updating '{node_name}'.")
install_result[ui_id] = f"An error occurred while updating '{node_name}'." nodepack_result[ui_id] = f"An error occurred while updating '{node_name}'."
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
install_result[ui_id] = f"An error occurred while updating '{node_name}'." nodepack_result[ui_id] = f"An error occurred while updating '{node_name}'."
async def do_fix(item): async def do_fix(item):
ui_id, node_name, node_ver = item ui_id, node_name, node_ver = item
@ -431,16 +433,16 @@ async def install_worker():
res = core.unified_manager.unified_fix(node_name, node_ver) res = core.unified_manager.unified_fix(node_name, node_ver)
if res.result: if res.result:
install_result[ui_id] = 'success' nodepack_result[ui_id] = 'success'
return return
else: else:
logging.error(res.msg) logging.error(res.msg)
logging.error(f"\nERROR: An error occurred while fixing '{node_name}@{node_ver}'.") logging.error(f"\nERROR: An error occurred while fixing '{node_name}@{node_ver}'.")
install_result[ui_id] = f"An error occurred while fixing '{node_name}@{node_ver}'." nodepack_result[ui_id] = f"An error occurred while fixing '{node_name}@{node_ver}'."
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
install_result[ui_id] = f"An error occurred while fixing '{node_name}@{node_ver}'." nodepack_result[ui_id] = f"An error occurred while fixing '{node_name}@{node_ver}'."
async def do_uninstall(item): async def do_uninstall(item):
ui_id, node_name, is_unknown = item ui_id, node_name, is_unknown = item
@ -449,14 +451,14 @@ async def install_worker():
res = core.unified_manager.unified_uninstall(node_name, is_unknown) res = core.unified_manager.unified_uninstall(node_name, is_unknown)
if res.result: if res.result:
install_result[ui_id] = 'success' nodepack_result[ui_id] = 'success'
return return
logging.error(f"\nERROR: An error occurred while uninstalling '{node_name}'.") logging.error(f"\nERROR: An error occurred while uninstalling '{node_name}'.")
install_result[ui_id] = f"An error occurred while uninstalling '{node_name}'." nodepack_result[ui_id] = f"An error occurred while uninstalling '{node_name}'."
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
install_result[ui_id] = f"An error occurred while uninstalling '{node_name}'." nodepack_result[ui_id] = f"An error occurred while uninstalling '{node_name}'."
async def do_disable(item): async def do_disable(item):
ui_id, node_name, is_unknown = item ui_id, node_name, is_unknown = item
@ -465,48 +467,96 @@ async def install_worker():
res = core.unified_manager.unified_disable(node_name, is_unknown) res = core.unified_manager.unified_disable(node_name, is_unknown)
if res: if res:
install_result[ui_id] = 'success' nodepack_result[ui_id] = 'success'
return return
install_result[ui_id] = f"Failed to disable: '{node_name}'" nodepack_result[ui_id] = f"Failed to disable: '{node_name}'"
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
install_result[ui_id] = f"Failed to disable: '{node_name}'" nodepack_result[ui_id] = f"Failed to disable: '{node_name}'"
async def do_install_model(item):
ui_id, json_data = item
model_path = get_model_path(json_data)
model_url = json_data['url']
try:
if model_path is not None:
logging.info(f"Install model '{json_data['name']}' from '{model_url}' into '{model_path}'")
if not core.get_config()['model_download_by_agent'] and (
model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')):
model_dir = get_model_dir(json_data, True)
download_url(model_url, model_dir, filename=json_data['filename'])
if model_path.endswith('.zip'):
res = core.unzip(model_path)
else:
res = True
if res:
model_result[ui_id] = 'success'
return
else:
res = download_url_with_agent(model_url, model_path)
if res and model_path.endswith('.zip'):
res = core.unzip(model_path)
else:
logging.error(f"Model installation error: invalid model type - {json_data['type']}")
return
if res:
model_result[ui_id] = 'success'
return
except Exception as e:
logging.error(f"[ERROR] {e}", file=sys.stderr)
model_result[ui_id] = f"Model installation error: {model_url}"
stats = {} stats = {}
while True: while True:
done_count = len(install_result) done_count = len(nodepack_result) + len(model_result)
total_count = done_count + install_queue.qsize() total_count = done_count + task_queue.qsize()
if install_queue.empty(): if task_queue.empty():
logging.info(f"\n[ComfyUI-Manager] Queued works are completed.\n{stats}") logging.info(f"\n[ComfyUI-Manager] Queued works are completed.\n{stats}")
logging.info("\nAfter restarting ComfyUI, please refresh the browser.") logging.info("\nAfter restarting ComfyUI, please refresh the browser.")
PromptServer.instance.send_sync("cm-install-status", PromptServer.instance.send_sync("cm-queue-status",
{'status': 'done', 'result': install_result, {'status': 'done',
'nodepack_result': nodepack_result, 'model_result': model_result,
'total_count': total_count, 'done_count': done_count}) 'total_count': total_count, 'done_count': done_count})
install_result = {} nodepack_result = {}
install_queue = queue.Queue() task_queue = queue.Queue()
return return
kind, item = install_queue.get() kind, item = task_queue.get()
if kind == 'install': try:
await do_install(item) if kind == 'install':
elif kind == 'update': await do_install(item)
await do_update(item) if kind == 'install-model':
elif kind == 'fix': await do_install_model(item)
await do_fix(item) elif kind == 'update':
elif kind == 'uninstall': await do_update(item)
await do_uninstall(item) elif kind == 'fix':
elif kind == 'disable': await do_fix(item)
await do_disable(item) elif kind == 'uninstall':
await do_uninstall(item)
elif kind == 'disable':
await do_disable(item)
except Exception:
traceback.print_exc()
stats[kind] = stats.get(kind, 0) + 1 stats[kind] = stats.get(kind, 0) + 1
PromptServer.instance.send_sync("cm-install-status", ui_target = "model_manager" if kind == 'install-model' else 'nodepack_manager'
{'status': 'in_progress', 'target': item[0],
print(f"kind: {kind} / ui_target: {ui_target}")
PromptServer.instance.send_sync("cm-queue-status",
{'status': 'in_progress', 'target': item[0], 'ui_target': ui_target,
'total_count': total_count, 'done_count': done_count}) 'total_count': total_count, 'done_count': done_count})
@ -1006,30 +1056,31 @@ async def import_fail_info(request):
return web.Response(status=400) return web.Response(status=400)
@routes.post("/customnode/reinstall") @routes.post("/manager/queue/reinstall")
async def reinstall_custom_node(request): async def reinstall_custom_node(request):
await uninstall_custom_node(request) await uninstall_custom_node(request)
await install_custom_node(request) await install_custom_node(request)
@routes.get("/customnode/queue/reset") @routes.get("/manager/queue/reset")
async def reset_queue(request): async def reset_queue(request):
global install_queue global task_queue
install_queue = queue.Queue() task_queue = queue.Queue()
return web.Response(status=200) return web.Response(status=200)
@routes.get("/customnode/queue/count") @routes.get("/manager/queue/status")
async def queue_count(request): async def queue_count(request):
global install_queue global task_queue
done_count = len(install_result) done_count = len(nodepack_result) + len(model_result)
total_count = done_count + install_queue.qsize() total_count = done_count + task_queue.qsize()
in_progress = task_worker_thread is not None and task_worker_thread.is_alive()
return web.json_response({'total_count': total_count, 'done_count': done_count}) return web.json_response({'total_count': total_count, 'done_count': done_count, 'in_progress': in_progress})
@routes.post("/customnode/queue/install") @routes.post("/manager/queue/install")
async def install_custom_node(request): async def install_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
@ -1073,22 +1124,32 @@ async def install_custom_node(request):
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs") return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install
install_queue.put(("install", install_item)) task_queue.put(("install", install_item))
return web.Response(status=200) return web.Response(status=200)
@routes.get("/customnode/queue/start") task_worker_thread = None
@routes.get("/manager/queue/start")
async def queue_start(request): async def queue_start(request):
global install_result global nodepack_result
install_result = {} global model_result
global task_worker_thread
threading.Thread(target=lambda: asyncio.run(install_worker())).start() if task_worker_thread is not None and task_worker_thread.is_alive():
return web.Response(status=201) # already in-progress
nodepack_result = {}
model_result = {}
task_worker_thread = threading.Thread(target=lambda: asyncio.run(task_worker()))
task_worker_thread.start()
return web.Response(status=200) return web.Response(status=200)
@routes.post("/customnode/queue/fix") @routes.post("/manager/queue/fix")
async def fix_custom_node(request): async def fix_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_GENERAL) logging.error(SECURITY_MESSAGE_GENERAL)
@ -1105,7 +1166,7 @@ async def fix_custom_node(request):
node_name = os.path.basename(json_data['files'][0]) node_name = os.path.basename(json_data['files'][0])
update_item = json_data.get('ui_id'), node_name, json_data['version'] update_item = json_data.get('ui_id'), node_name, json_data['version']
install_queue.put(("fix", update_item)) task_queue.put(("fix", update_item))
return web.Response(status=200) return web.Response(status=200)
@ -1142,7 +1203,7 @@ async def install_custom_node_pip(request):
return web.Response(status=200) return web.Response(status=200)
@routes.post("/customnode/queue/uninstall") @routes.post("/manager/queue/uninstall")
async def uninstall_custom_node(request): async def uninstall_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
@ -1160,12 +1221,12 @@ async def uninstall_custom_node(request):
node_name = os.path.basename(json_data['files'][0]) node_name = os.path.basename(json_data['files'][0])
uninstall_item = json_data.get('ui_id'), node_name, is_unknown uninstall_item = json_data.get('ui_id'), node_name, is_unknown
install_queue.put(("uninstall", uninstall_item)) task_queue.put(("uninstall", uninstall_item))
return web.Response(status=200) return web.Response(status=200)
@routes.post("/customnode/queue/update") @routes.post("/manager/queue/update")
async def update_custom_node(request): async def update_custom_node(request):
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
@ -1181,7 +1242,7 @@ async def update_custom_node(request):
node_name = os.path.basename(json_data['files'][0]) node_name = os.path.basename(json_data['files'][0])
update_item = json_data.get('ui_id'), node_name, json_data['version'] update_item = json_data.get('ui_id'), node_name, json_data['version']
install_queue.put(("update", update_item)) task_queue.put(("update", update_item))
return web.Response(status=200) return web.Response(status=200)
@ -1232,7 +1293,7 @@ async def comfyui_switch_version(request):
return web.Response(status=400) return web.Response(status=400)
@routes.post("/customnode/queue/disable") @routes.post("/manager/queue/disable")
async def disable_node(request): async def disable_node(request):
json_data = await request.json() json_data = await request.json()
@ -1246,7 +1307,7 @@ async def disable_node(request):
node_name = os.path.basename(json_data['files'][0]) node_name = os.path.basename(json_data['files'][0])
update_item = json_data.get('ui_id'), node_name, is_unknown update_item = json_data.get('ui_id'), node_name, is_unknown
install_queue.put(("disable", update_item)) task_queue.put(("disable", update_item))
return web.Response(status=200) return web.Response(status=200)
@ -1264,12 +1325,10 @@ async def need_to_migrate(request):
return web.Response(text=str(core.need_to_migrate), status=200) return web.Response(text=str(core.need_to_migrate), status=200)
@routes.post("/model/install") @routes.post("/manager/queue/install_model")
async def install_model(request): async def install_model(request):
json_data = await request.json() json_data = await request.json()
model_path = get_model_path(json_data)
if not is_allowed_security_level('middle'): if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW) logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403) return web.Response(status=403)
@ -1287,42 +1346,8 @@ async def install_model(request):
logging.error(SECURITY_MESSAGE_NORMAL_MINUS) logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
return web.Response(status=403) return web.Response(status=403)
def do_install(): install_item = json_data.get('ui_id'), json_data
res = False task_queue.put(("install-model", install_item))
try:
if model_path is not None:
model_url = json_data['url']
logging.info(f"Install model '{json_data['name']}' from '{model_url}' into '{model_path}'")
if not core.get_config()['model_download_by_agent'] and (
model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')):
model_dir = get_model_dir(json_data, True)
download_url(model_url, model_dir, filename=json_data['filename'])
if model_path.endswith('.zip'):
res = core.unzip(model_path)
else:
res = True
if res:
return web.json_response({}, content_type='application/json')
else:
res = download_url_with_agent(model_url, model_path)
if res and model_path.endswith('.zip'):
res = core.unzip(model_path)
else:
logging.error(f"Model installation error: invalid model type - {json_data['type']}")
if res:
return web.json_response({}, content_type='application/json')
except Exception as e:
logging.error(f"[ERROR] {e}", file=sys.stderr)
return web.Response(status=400)
# Run the installation in a thread pool
with concurrent.futures.ThreadPoolExecutor() as executor:
asyncio.get_event_loop().run_in_executor(executor, do_install)
return web.Response(status=200) return web.Response(status=200)

View File

@ -55,6 +55,12 @@ const pageCss = `
color: white; color: white;
} }
.cn-manager .cn-manager-stop {
display: none;
background-color: #500000;
color: white;
}
.cn-manager .cn-manager-back { .cn-manager .cn-manager-back {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -344,13 +350,14 @@ const pageHtml = `
<div class="cn-manager-selection"></div> <div class="cn-manager-selection"></div>
<div class="cn-manager-message"></div> <div class="cn-manager-message"></div>
<div class="cn-manager-footer"> <div class="cn-manager-footer">
<button class="cn-manager-back"> <button class="cn-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
Back Back
</button> </button>
<button class="cn-manager-restart">Restart</button> <button class="cn-manager-restart">Restart</button>
<button class="cn-manager-stop">Stop</button>
<div class="cn-flex-auto"></div> <div class="cn-flex-auto"></div>
<button class="cn-manager-check-update">Check Update</button> <button class="cn-manager-check-update">Check Update</button>
<button class="cn-manager-check-missing">Check Missing</button> <button class="cn-manager-check-missing">Check Missing</button>
@ -392,7 +399,7 @@ export class CustomNodesManager {
this.init(); this.init();
api.addEventListener("cm-install-status", this.onInstallStatus); api.addEventListener("cm-queue-status", this.onQueueStatus);
} }
init() { init() {
@ -762,6 +769,13 @@ export class CustomNodesManager {
} }
}, },
".cn-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
".cn-manager-check-update": { ".cn-manager-check-update": {
click: (e) => { click: (e) => {
e.target.classList.add("cn-btn-loading"); e.target.classList.add("cn-btn-loading");
@ -1271,9 +1285,9 @@ export class CustomNodesManager {
} }
async installNodes(list, btn, title, selected_version) { async installNodes(list, btn, title, selected_version) {
let stats = await api.fetchApi('/customnode/queue/count'); let stats = await api.fetchApi('/manager/queue/status');
stats = await stats.json(); stats = await stats.json();
if(stats.total_count > 0) { if(stats.in_progress) {
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`); customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
return; return;
} }
@ -1304,11 +1318,13 @@ export class CustomNodesManager {
let needRestart = false; let needRestart = false;
let errorMsg = ""; let errorMsg = "";
await api.fetchApi('/customnode/queue/reset'); await api.fetchApi('/manager/queue/reset');
this.install_context = btn;
let target_items = [];
for (const hash of list) { for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash); const item = this.grid.getRowItemBy("hash", hash);
target_items.push(item);
if (!item) { if (!item) {
errorMsg = `Not found custom node: ${hash}`; errorMsg = `Not found custom node: ${hash}`;
@ -1347,7 +1363,7 @@ export class CustomNodesManager {
api_mode = 'reinstall'; api_mode = 'reinstall';
} }
const res = await api.fetchApi(`/customnode/queue/${api_mode}`, { const res = await api.fetchApi(`/manager/queue/${api_mode}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
@ -1367,18 +1383,32 @@ export class CustomNodesManager {
} }
} }
this.install_context = {btn: btn, targets: target_items};
for(let k in target_items) {
let item = this.install_context.targets[k];
this.grid.updateCell(item, "action");
}
if(errorMsg) { if(errorMsg) {
this.showError(errorMsg); this.showError(errorMsg);
show_message("Installation Error:\n"+errorMsg); show_message("Installation Error:\n"+errorMsg);
// reset
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
self.grid.updateCell(item, "action");
}
} }
else { else {
await api.fetchApi('/customnode/queue/start'); await api.fetchApi('/manager/queue/start');
this.showStop();
} }
} }
async onInstallStatus(event) { async onQueueStatus(event) {
let self = CustomNodesManager.instance; let self = CustomNodesManager.instance;
if(event.detail.status == 'in_progress') { if(event.detail.status == 'in_progress' && event.detail.ui_target == 'nodepack_manager') {
const hash = event.detail.target; const hash = event.detail.target;
const item = self.grid.getRowItemBy("hash", hash); const item = self.grid.getRowItemBy("hash", hash);
@ -1386,14 +1416,20 @@ export class CustomNodesManager {
item.restart = true; item.restart = true;
self.restartMap[item.hash] = true; self.restartMap[item.hash] = true;
self.grid.updateCell(item, "action"); self.grid.updateCell(item, "action");
self.grid.setRowSelected(item, false);
} }
else if(event.detail.status == 'done') { else if(event.detail.status == 'done') {
self.onInstallCompleted(event.detail); self.hideStop();
self.onQueueCompleted(event.detail);
} }
} }
async onInstallCompleted(info) { async onQueueCompleted(info) {
let result = info.result; let result = info.nodepack_result;
if(result.length == 0) {
return;
}
let self = CustomNodesManager.instance; let self = CustomNodesManager.instance;
@ -1401,7 +1437,7 @@ export class CustomNodesManager {
return; return;
} }
const { target, label, mode } = self.install_context; const { target, label, mode } = self.install_context.btn;
target.classList.remove("cn-btn-loading"); target.classList.remove("cn-btn-loading");
let errorMsg = ""; let errorMsg = "";
@ -1409,13 +1445,15 @@ export class CustomNodesManager {
for(let hash in result){ for(let hash in result){
let v = result[hash]; let v = result[hash];
const item = self.grid.getRowItemBy("hash", hash);
self.grid.setRowSelected(item, false);
if(v != 'success') if(v != 'success')
errorMsg += v; errorMsg += v;
} }
for(let k in self.install_context.targets) {
let item = self.install_context.targets[k];
self.grid.updateCell(item, "action");
}
if (errorMsg) { if (errorMsg) {
self.showError(errorMsg); self.showError(errorMsg);
show_message("Installation Error:\n"+errorMsg); show_message("Installation Error:\n"+errorMsg);
@ -1426,7 +1464,7 @@ export class CustomNodesManager {
self.showRestart(); self.showRestart();
self.showMessage(`To apply the installed/updated/disabled/enabled custom node, please restart ComfyUI. And refresh browser.`, "red"); self.showMessage(`To apply the installed/updated/disabled/enabled custom node, please restart ComfyUI. And refresh browser.`, "red");
infoToast(`[ComfyUI-Manager] All tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`); infoToast(`[ComfyUI-Manager] All node pack tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`);
self.install_context = undefined; self.install_context = undefined;
} }
@ -1808,9 +1846,9 @@ export class CustomNodesManager {
} }
setDisabled(disabled) { setDisabled(disabled) {
const $close = this.element.querySelector(".cn-manager-close"); const $close = this.element.querySelector(".cn-manager-close");
const $restart = this.element.querySelector(".cn-manager-restart"); const $restart = this.element.querySelector(".cn-manager-restart");
const $stop = this.element.querySelector(".cn-manager-stop");
const list = [ const list = [
".cn-manager-header input", ".cn-manager-header input",
@ -1822,7 +1860,7 @@ export class CustomNodesManager {
}) })
.flat() .flat()
.filter(it => { .filter(it => {
return it !== $close && it !== $restart; return it !== $close && it !== $restart && it !== $stop;
}); });
list.forEach($elem => { list.forEach($elem => {
@ -1843,6 +1881,14 @@ export class CustomNodesManager {
this.element.querySelector(".cn-manager-restart").style.display = "block"; this.element.querySelector(".cn-manager-restart").style.display = "block";
} }
showStop() {
this.element.querySelector(".cn-manager-stop").style.display = "block";
}
hideStop() {
this.element.querySelector(".cn-manager-stop").style.display = "none";
}
setFilter(filterValue) { setFilter(filterValue) {
let filter = ""; let filter = "";
const filterItem = this.getFilterItem(filterValue); const filterItem = this.getFilterItem(filterValue);

View File

@ -1,8 +1,10 @@
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js"; import { $el } from "../../scripts/ui.js";
import { import {
manager_instance, rebootAPI, manager_instance, rebootAPI,
fetchData, md5, icons fetchData, md5, icons, show_message, customAlert, infoToast
} from "./common.js"; } from "./common.js";
import { api } from "../../scripts/api.js";
// https://cenfun.github.io/turbogrid/api.html // https://cenfun.github.io/turbogrid/api.html
import TG from "./turbogrid.esm.js"; import TG from "./turbogrid.esm.js";
@ -46,6 +48,18 @@ const pageCss = `
background-color: var(--comfy-input-bg); background-color: var(--comfy-input-bg);
} }
.cmm-manager .cmm-manager-refresh {
display: none;
background-color: #000080;
color: white;
}
.cmm-manager .cmm-manager-stop {
display: none;
background-color: #500000;
color: white;
}
.cmm-manager-header { .cmm-manager-header {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -235,7 +249,14 @@ const pageHtml = `
<div class="cmm-manager-selection"></div> <div class="cmm-manager-selection"></div>
<div class="cmm-manager-message"></div> <div class="cmm-manager-message"></div>
<div class="cmm-manager-footer"> <div class="cmm-manager-footer">
<button class="cmm-manager-back">Back</button> <button class="cmm-manager-back">
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back
</button>
<button class="cmm-manager-refresh">Refresh</button>
<button class="cmm-manager-stop">Stop</button>
<div class="cmm-flex-auto"></div> <div class="cmm-flex-auto"></div>
</div> </div>
`; `;
@ -254,6 +275,8 @@ export class ModelManager {
this.keywords = ''; this.keywords = '';
this.init(); this.init();
api.addEventListener("cm-queue-status", this.onQueueStatus);
} }
init() { init() {
@ -365,12 +388,25 @@ export class ModelManager {
} }
}, },
".cmm-manager-refresh": {
click: () => {
app.refreshComboInNodes();
}
},
".cmm-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
".cmm-manager-back": { ".cmm-manager-back": {
click: (e) => { click: (e) => {
this.close() this.close()
manager_instance.show(); manager_instance.show();
} }
}, }
}; };
Object.keys(eventsMap).forEach(selector => { Object.keys(eventsMap).forEach(selector => {
const target = this.element.querySelector(selector); const target = this.element.querySelector(selector);
@ -595,17 +631,28 @@ export class ModelManager {
} }
async installModels(list, btn) { async installModels(list, btn) {
let stats = await api.fetchApi('/manager/queue/status');
stats = await stats.json();
if(stats.in_progress) {
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
return;
}
btn.classList.add("cmm-btn-loading"); btn.classList.add("cmm-btn-loading");
this.showLoading(); this.showLoading();
this.showError(""); this.showError("");
let needRestart = false; let needRefresh = false;
let errorMsg = ""; let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
let target_items = [];
for (const item of list) { for (const item of list) {
this.grid.scrollRowIntoView(item); this.grid.scrollRowIntoView(item);
target_items.push(item);
if (!this.focusInstall(item)) { if (!this.focusInstall(item)) {
this.grid.onNextUpdated(() => { this.grid.onNextUpdated(() => {
@ -616,48 +663,104 @@ export class ModelManager {
this.showStatus(`Install ${item.name} ...`); this.showStatus(`Install ${item.name} ...`);
const data = item.originalData; const data = item.originalData;
const res = await fetchData('/model/install', { data.ui_id = item.hash;
const res = await api.fetchApi(`/manager/queue/install_model`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
if (res.status != 200) {
if (res.error) {
errorMsg = `Install failed: ${item.name} ${res.error.message}`; errorMsg = `Install failed: ${item.name} ${res.error.message}`;
break;; break;
} }
}
needRestart = true; this.install_context = {btn: btn, targets: target_items};
this.grid.setRowSelected(item, false); if(errorMsg) {
this.showError(errorMsg);
show_message("Installation Error:\n"+errorMsg);
// reset
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
this.grid.updateCell(item, "installed");
}
}
else {
await api.fetchApi('/manager/queue/start');
this.showStop();
}
}
async onQueueStatus(event) {
let self = ModelManager.instance;
if(event.detail.status == 'in_progress' && event.detail.ui_target == 'model_manager') {
const hash = event.detail.target;
const item = self.grid.getRowItemBy("hash", hash);
item.refresh = true; item.refresh = true;
self.grid.setRowSelected(item, false);
item.selectable = false; item.selectable = false;
this.grid.updateCell(item, "installed"); // self.grid.updateCell(item, "tg-column-select");
this.grid.updateCell(item, "tg-column-select"); self.grid.updateRow(item);
}
else if(event.detail.status == 'done') {
self.hideStop();
self.onQueueCompleted(event.detail);
}
}
this.showStatus(`Install ${item.name} successfully`); async onQueueCompleted(info) {
let result = info.model_result;
if(result.length == 0) {
return;
} }
this.hideLoading(); let self = ModelManager.instance;
if(!self.install_context) {
return;
}
let btn = self.install_context.btn;
self.hideLoading();
btn.classList.remove("cmm-btn-loading"); btn.classList.remove("cmm-btn-loading");
let errorMsg = "";
for(let hash in result){
let v = result[hash];
if(v != 'success')
errorMsg += v;
}
for(let k in self.install_context.targets) {
let item = self.install_context.targets[k];
self.grid.updateCell(item, "installed");
}
if (errorMsg) { if (errorMsg) {
this.showError(errorMsg); self.showError(errorMsg);
show_message("Installation Error:\n"+errorMsg);
} else { } else {
this.showStatus(`Install ${list.length} models successfully`); self.showStatus(`Install ${result.length} models successfully`);
} }
if (needRestart) { self.showRefresh();
this.showMessage(`To apply the installed model, please click the 'Refresh' button on the main menu.`, "red") self.showMessage(`To apply the installed model, please click the 'Refresh' button.`, "red")
}
infoToast('Tasks done', `[ComfyUI-Manager] All model downloading tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`);
self.install_context = undefined;
} }
getModelList(models) { getModelList(models) {
const typeMap = new Map(); const typeMap = new Map();
const baseMap = new Map(); const baseMap = new Map();
@ -826,7 +929,7 @@ export class ModelManager {
} }
showLoading() { showLoading() {
this.setDisabled(true); // this.setDisabled(true);
if (this.grid) { if (this.grid) {
this.grid.showLoading(); this.grid.showLoading();
this.grid.showMask({ this.grid.showMask({
@ -836,7 +939,7 @@ export class ModelManager {
} }
hideLoading() { hideLoading() {
this.setDisabled(false); // this.setDisabled(false);
if (this.grid) { if (this.grid) {
this.grid.hideLoading(); this.grid.hideLoading();
this.grid.hideMask(); this.grid.hideMask();
@ -844,8 +947,9 @@ export class ModelManager {
} }
setDisabled(disabled) { setDisabled(disabled) {
const $close = this.element.querySelector(".cmm-manager-close"); const $close = this.element.querySelector(".cmm-manager-close");
const $refresh = this.element.querySelector(".cmm-manager-refresh");
const $stop = this.element.querySelector(".cmm-manager-stop");
const list = [ const list = [
".cmm-manager-header input", ".cmm-manager-header input",
@ -857,7 +961,7 @@ export class ModelManager {
}) })
.flat() .flat()
.filter(it => { .filter(it => {
return it !== $close; return it !== $close && it !== $refresh && it !== $stop;
}); });
list.forEach($elem => { list.forEach($elem => {
@ -874,6 +978,18 @@ export class ModelManager {
} }
showRefresh() {
this.element.querySelector(".cmm-manager-refresh").style.display = "block";
}
showStop() {
this.element.querySelector(".cmm-manager-stop").style.display = "block";
}
hideStop() {
this.element.querySelector(".cmm-manager-stop").style.display = "none";
}
setKeywords(keywords = "") { setKeywords(keywords = "") {
this.keywords = keywords; this.keywords = keywords;
this.element.querySelector(".cmm-manager-keywords").value = keywords; this.element.querySelector(".cmm-manager-keywords").value = keywords;

View File

@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-manager" name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI." description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.12.2" version = "3.13"
license = { file = "LICENSE.txt" } license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"] dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"]