From ec3aa22a4a961243c8819bffb7b1b75a63afcd0b Mon Sep 17 00:00:00 2001 From: cavinHuang Date: Sat, 25 May 2024 18:03:20 +0800 Subject: [PATCH] feat: add setting & import docs & exports --- .cache/.cache_directory | 0 .cache/.setting.json | 1 + .gitignore | 5 +- README.md | 1 + README_zh.md | 11 +- __init__.py | 24 +--- pyproject.toml | 2 +- server/__init__.py | 1 + server/request.py | 296 ++++++++++++++++++++++++++++++++++++++++ web/comfyui/index.js | 263 +++++++++++++++++++++++++++++++---- web/comfyui/utils.js | 14 ++ 11 files changed, 566 insertions(+), 52 deletions(-) create mode 100644 .cache/.cache_directory create mode 100644 .cache/.setting.json create mode 100644 server/__init__.py create mode 100644 server/request.py create mode 100644 web/comfyui/utils.js diff --git a/.cache/.cache_directory b/.cache/.cache_directory new file mode 100644 index 00000000..e69de29b diff --git a/.cache/.setting.json b/.cache/.setting.json new file mode 100644 index 00000000..54b93112 --- /dev/null +++ b/.cache/.setting.json @@ -0,0 +1 @@ +{"contribute": true} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3eae904b..22322cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ __pycache__ *.ini .vscode/ .idea/ -node_modules/ \ No newline at end of file +node_modules/ +.cache/** +!.cache/.cache_directory +!.cache/.setting.json diff --git a/README.md b/README.md index 658201be..dc324fe4 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,4 @@ Node output types Node source code + diff --git a/README_zh.md b/README_zh.md index 2d2e2b23..f1286cb9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -70,4 +70,13 @@ Node output types # Source code Node source code - \ No newline at end of file + + +## 更新日志 + +### 2024-05-25 +- 在设置中增加了开关,可以选择是否显示节点的文档 +- 增加文档本地修改功能,如果觉得文档有问题,可以在本地修改,不会影响到其他人 +- 在设置中增加了是否参与共建的开关,可以选择是否参与共建,默认打开,打开后会把本地修改的记录,记录到云DB上,后期经过审核后会合并到主分支上 +- 增加导出文档和导入文档功能,导出文档会把本地修改的记录和仓库提供的文档导出下载,导入文档会把导出的文档导入到本地,不会影响主仓库的文档。 +- 修复了一些bug \ No newline at end of file diff --git a/__init__.py b/__init__.py index 4e5f97df..b5a78a36 100644 --- a/__init__.py +++ b/__init__.py @@ -1,31 +1,13 @@ -import os -from server import PromptServer -from aiohttp import web +from .server import * WEB_DIRECTORY = "./web/comfyui" NODE_CLASS_MAPPINGS = {} __all__ = ['WEB_DIRECTORY', 'NODE_CLASS_MAPPINGS'] -CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) - -@PromptServer.instance.routes.get("/customnode/getNodeInfo") -async def fetch_customnode_node_info(request): - try: - node_name = request.rel_url.query["nodeName"] - - if not node_name: - return web.json_response({"content": ""}) - - file_path = os.path.join(CURRENT_DIR, 'docs', node_name + '.md') - if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf-8') as file: - return web.json_response({"content": file.read()}) - else: - return web.json_response({"content": ""}) - except Exception as e: - return web.json_response({"content": ""}) MAGENTA = '\033[95m' RESET = '\033[0m' +print(f"{MAGENTA}==============================================") print(f"{MAGENTA}[comfyui-nodes-docs]{RESET}Loaded comfyui-nodes-docs plugin") +print(f"{MAGENTA}=============================================={RESET}") diff --git a/pyproject.toml b/pyproject.toml index 0e1d40ce..8059d1c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-nodes-docs" description = "This is a plugin for displaying documentation for each comfyui node. " -version = "1.0.0" +version = "1.1.0" license = "LICENSE" [project.urls] diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 00000000..329ca6b8 --- /dev/null +++ b/server/__init__.py @@ -0,0 +1 @@ +from .request import * \ No newline at end of file diff --git a/server/request.py b/server/request.py new file mode 100644 index 00000000..a043af2b --- /dev/null +++ b/server/request.py @@ -0,0 +1,296 @@ + +import json +import os +from server import PromptServer +from aiohttp import web,ClientSession +import asyncio +import zipfile +import tempfile + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + +root_dir = os.path.join(CURRENT_DIR, '..') + +cache_dir = os.path.join(root_dir, '.cache') +docs_dir = os.path.join(root_dir, 'docs') +setting_path = os.path.join(cache_dir, '.setting.json') +device_id_path = os.path.join(cache_dir, '.cache_d_id') + +# 根据设备名称和设备信息 ip地址,创建一个唯一的设备ID +def create_device_id(): + import hashlib + import socket + import uuid + + # 获取设备名称 + device_name = socket.gethostname() + # 获取设备IP地址 + device_ip = socket.gethostbyname(device_name) + # 获取设备唯一标识 + device_uuid = str(uuid.getnode()) + # 创建设备ID + device_id = hashlib.md5((device_name + device_ip + device_uuid ).encode('utf-8')).hexdigest() + return device_id + +if not os.path.exists(cache_dir): + os.makedirs(cache_dir, 755) + +# 获取节点文档内容 +def get_node_doc_file_content(node_name): + file_path = os.path.join(docs_dir, node_name + '.md') + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + return file.read() + else: + return "" + +# 获取节点缓存文档 +def get_node_cache_file_content(node_name): + file_path = os.path.join(cache_dir, node_name + '.md') + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as file: + return file.read() + else: + return "" + +# 写入缓存文件 +def write_cache_file(node_name, content): + file_path = os.path.join(cache_dir, node_name + '.md') + print(file_path) + with open(file_path, 'w', encoding='utf-8') as file: + file.write(content) + +def write_device_id(): + # 创建设备ID + device_id = create_device_id() + # 写入缓存文件 + with open(device_id_path, 'w', encoding='utf-8') as file: + file.write(device_id) + return device_id + +write_device_id() + +def get_device_id(): + if os.path.exists(device_id_path): + with open(device_id_path, 'r', encoding='utf-8') as file: + return file.read() + else: + divce_id = write_device_id() + return divce_id + +# Add route to fetch node info +@PromptServer.instance.routes.get("/customnode/getNodeInfo") +async def fetch_customnode_node_info(request): + try: + node_name = request.rel_url.query["nodeName"] + + if not node_name: + return web.json_response({"content": ""}) + + cache_content = get_node_cache_file_content(node_name) + + if len(cache_content) > 0 : + return web.json_response({"content": cache_content}) + + content = get_node_doc_file_content(node_name) + return web.json_response({"content": content}) + except Exception as e: + return web.json_response({"content": ""}) + +# Add route to cache node info +@PromptServer.instance.routes.get("/customnode/cacheNodeInfo") +async def cache_customnode_node_info(request): + try: + node_name = request.rel_url.query["nodeName"] + + if not node_name: + return web.json_response({"success": False, "content": ""}) + + print('cache start') + if not os.path.exists(os.path.join(cache_dir, node_name + '.md')): + print('cache file not exists') + content = get_node_doc_file_content(node_name) + write_cache_file(node_name, content) + print('cache success') + return web.json_response({"success": True, "content": content}) + return web.json_response({"success": True, "content": ''}) + except Exception as e: + print(e) + return web.json_response({"success": False, "content": ''}) + +# Add route to update node info +@PromptServer.instance.routes.post("/customnode/updateNodeInfo") +async def update_customnode_node_info(request): + try: + json_data = await request.json() + node_name = json_data["nodeName"] + # node_name = request.rel_url.query["nodeName"] + content = json_data["content"] + + if not node_name: + return web.json_response({"success": False}) + + write_cache_file(node_name, content) + + contribute = get_setting_item('contribute') + if contribute == True: + print('send doc to cloud') + asyncio.create_task(send_doc_to_cloud(node_name, content)) + + return web.json_response({"success": True}) + except Exception as e: + return web.json_response({"success": False}) + + +# ================================== 以下是导出节点文档的代码 ================================== +def get_all_files(directory): + """ 获取目录下所有文件的路径 """ + file_paths = [] + for root, _, files in os.walk(directory): + for file in files: + file_paths.append(os.path.join(root, file)) + return file_paths + +def collect_unique_files(dir1, dir2): + """ 收集两个目录下的所有文件,按文件名去重 """ + unique_files = {} + for file_path in get_all_files(dir1) + get_all_files(dir2): + file_name = os.path.basename(file_path) + if file_name not in unique_files: + unique_files[file_name] = file_path + return unique_files.values() + +def zip_files(file_paths, output_zip): + """ 将文件打包成ZIP,并平铺存储 """ + with zipfile.ZipFile(output_zip, 'w') as zipf: + for file_path in file_paths: + arcname = os.path.basename(file_path) # 只使用文件名,不保留路径 + zipf.write(file_path, arcname=arcname) + +# Add route to export node info +@PromptServer.instance.routes.get("/customnode/exportNodeInfo") +async def export_customnode_node_info(request): + # 把所有的缓存文件和docs文件夹下的文件打包成zip文件 + """ 生成ZIP文件并返回文件流响应 """ + + unique_files = collect_unique_files(cache_dir, docs_dir) + + # 使用临时文件存储ZIP + temp_zip = tempfile.NamedTemporaryFile(delete=False) + try: + with zipfile.ZipFile(temp_zip, 'w') as zipf: + for file_path in unique_files: + arcname = os.path.basename(file_path) + zipf.write(file_path, arcname=arcname) + + temp_zip.seek(0) + + # 生成StreamResponse响应 + response = web.StreamResponse() + response.headers['Content-Type'] = 'application/zip' + response.headers['Content-Disposition'] = 'attachment; filename="output.zip"' + + await response.prepare(request) + + with open(temp_zip.name, 'rb') as f: + while chunk := f.read(8192): + await response.write(chunk) + + await response.write_eof() + print(response) + return response + except Exception as e: + print(e) + finally: + os.remove(temp_zip.name) # 删除临时文件 + + +# Add route to import node info +@PromptServer.instance.routes.post("/customnode/importNodeInfo") +async def import_customnode_node_info(request): + # 接收上传的ZIP文件并解压到指定目录 + """ 接收上传的ZIP文件并解压到指定目录 """ + try: + data = await request.post() + zip_file = data.get('file') + + # 保存上传的ZIP文件 + zip_path = os.path.join(CURRENT_DIR, 'upload.zip') + with open(zip_path, 'wb') as f: + f.write(zip_file.file.read()) + + # 解压ZIP文件到.cache目录,并且覆盖原有文件 + with zipfile.ZipFile(zip_path, 'r') as zipf: + zipf.extractall(cache_dir) + + os.remove(zip_path) # 删除上传的ZIP文件 + return web.json_response({"success": True}) + except Exception as e: + return web.json_response({"success": False}) + + +# 获取设置 +def get_setting(): + if os.path.exists(setting_path): + with open(setting_path, 'r', encoding='utf-8') as file: + return file.read() + else: + return "{}" + +# 获取设置的每一项 +def get_setting_item(key): + setting = get_setting() + setting_json = json.loads(setting) + return setting_json[key] + +# 保存设置到文件 +def save_setting(setting): + with open(setting_path, 'w', encoding='utf-8') as file: + file.write(setting) + +if not os.path.exists(setting_path): + save_setting('{"contribute": true}') + +# 更新设置到本地.setting.json +@PromptServer.instance.routes.post("/customnode/updateSetting") +async def update_setting(request): + try: + json_data = await request.json() + setting = json_data + # 跟缓存文件的设置项每一项对比,如果有不同则更新 + cache_setting = get_setting() + cache_setting_json = json.loads(cache_setting) + for key in setting: + cache_setting_json[key] = setting[key] + + # 保存设置到文件,缩近2空格 + save_setting(json.dumps(cache_setting_json, indent=2)) + + return web.json_response({"success": True}) + except Exception as e: + return web.json_response({"success": False}) + +# 发送当前文档到云 +async def send_doc_to_cloud(node_type, content): + url = 'http://comfy.zukmb.cn/api/saveNodesDocs' + async with ClientSession() as session: + device_id = get_device_id() + data = { + 'device_id': device_id, + 'node_type': node_type, + 'content': content + } + print('send doc to cloud') + async with session.post(url, data=data) as response: + print(await response.text()) + # try: + + # # url = 'http://localhost:8080/api/saveNodesDocs' + # # data = { + # # 'device_id': device_id, + # # 'node_type': node_type, + # # 'content': content + # # } + # # requests.post(url, data=data) + # except Exception as e: + # print(e) \ No newline at end of file diff --git a/web/comfyui/index.js b/web/comfyui/index.js index 40ec9f34..1420f0f8 100644 --- a/web/comfyui/index.js +++ b/web/comfyui/index.js @@ -1,7 +1,10 @@ +//@ts-nocheck +import {$el} from "../../scripts/ui.js"; import { app } from "../../scripts/app.js"; import { marked } from './marked.js' +import { throttle } from './utils.js' -console.log('app', app) +const ENABLED_SETTING_KEY = 'comfyui-nodes-docs.enabled' /** * 1: { @@ -66,6 +69,42 @@ const hideActiveDocs = function() { } } +// 缓存到本地 +const fetchCacheDNodeDoc = async function(nodeName) { + const res = await fetch('/customnode/cacheNodeInfo?nodeName=' + nodeName) + const jsonData = await res.json() + return jsonData +} + +// 保存文档到本地 +const saveNodeDoc = async function(nodeName, content) { + const res = await fetch('/customnode/updateNodeInfo', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + nodeName, + content + }) + }) + + return await res.json() +} + +// 更新设置文件 +const updateSettingFile = async function(setting) { + const res = await fetch('/customnode/updateSetting', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(setting) + }) + const jsonData = await res.json() + return jsonData +} + /** * 显示节点文档 * @param {*} node @@ -73,7 +112,7 @@ const hideActiveDocs = function() { */ const showNodeDocs = async function(node) { const ele = nodeDocsEleMap.get(node.id) - const [nLeft, nTop, nWidth, nHeight] = node.getBounding() + // const [nLeft, nTop, nWidth, nHeight] = node.getBounding() if(ele) { ele.style.display = 'block' // 更新位置 @@ -99,13 +138,6 @@ const showNodeDocs = async function(node) { document.body.appendChild(divWrap) const buttonClose = document.createElement('button') - /** - background-color: rgba(0, 0, 0, 0); - padding: 0; - border: none; - cursor: pointer; - font-size: inherit; - */ buttonClose.style.backgroundColor = 'rgba(0, 0, 0, 0)' buttonClose.style.padding = '0' buttonClose.style.border = 'none' @@ -127,10 +159,10 @@ const showNodeDocs = async function(node) { const divContentWrap = document.createElement('div') divContentWrap.style.background = 'var(--comfy-input-bg)' - divContentWrap.style.height = 'calc(100% - 44px)' + divContentWrap.style.height = 'calc(100% - 44px - 70px)' divContentWrap.style.padding = '10px' divContentWrap.style.borderRadius = '10px' - divContentWrap.style.overflowX = 'hidden' + divContentWrap.style.overflow = 'hidden' divContentWrap.style.overflowY = 'auto' divWrap.appendChild(divButtonWrap) @@ -144,6 +176,87 @@ const showNodeDocs = async function(node) { divContentWrap.innerHTML = html || node.description || '暂无文档' + // 编辑框 + const editWrap = document.createElement('div') + editWrap.style.display = 'none' + editWrap.style.padding = '20px' + editWrap.borderRadius = '10px' + editWrap.style.backgroundColor = 'var(--comfy-menu-bg)' + editWrap.style.color = 'white' + editWrap.style.height = 'calc(100% - 44px - 70px)' + editWrap.style.boxSizing = 'border-box' + const editInput = document.createElement('textarea') + editInput.style.width = '100%' + editInput.style.height = '100%' + editInput.style.backgroundColor = 'var(--comfy-input-bg)' + editInput.style.color = 'white' + editInput.style.border = 'none' + editInput.style.borderRadius = '10px' + editInput.style.padding = '10px' + editInput.style.resize = 'none' + editInput.style.fontSize = '16px' + editInput.style.lineHeight = '1.5' + editInput.value = jsonData.content + editWrap.appendChild(editInput) + divWrap.appendChild(editWrap) + + // 增加按钮 + const buttonWrap = document.createElement('div') + buttonWrap.style.display = 'flex' + buttonWrap.style.justifyContent = 'flex-end' + buttonWrap.style.padding = '20px' + + const editButton = document.createElement('button') + editButton.innerText = '编辑' + editButton.style.backgroundColor = 'var(--theme-color)' + editButton.style.color = 'white' + editButton.style.padding = '5px 10px' + editButton.style.border = 'none' + editButton.style.cursor = 'pointer' + editButton.style.borderRadius = '5px' + + const cancelEditButton = document.createElement('button') + cancelEditButton.innerText = '取消' + cancelEditButton.style.backgroundColor = 'var(--comfy-input-bg)' + cancelEditButton.style.color = 'white' + cancelEditButton.style.padding = '5px 10px' + cancelEditButton.style.border = 'none' + cancelEditButton.style.cursor = 'pointer' + cancelEditButton.style.display = 'none' + cancelEditButton.style.marginLeft = '16px' + editButton.style.borderRadius = '5px' + + cancelEditButton.onclick = function() { + editWrap.style.display = 'none' + divContentWrap.style.display = 'block' + editButton.innerText = '编辑' + cancelEditButton.style.display = 'none' + } + + editButton.onclick = function() { + if(editWrap.style.display === 'none') { + fetchCacheDNodeDoc(node.type) + editWrap.style.display = 'block' + divContentWrap.style.display = 'none' + editButton.innerText = '保存' + cancelEditButton.style.display = 'block' + } else { + saveNodeDoc(node.type, editInput.value).then(res => { + if (res.success) { + divContentWrap.innerHTML = marked.parse(editInput.value) + editWrap.style.display = 'none' + divContentWrap.style.display = 'block' + editButton.innerText = '编辑' + cancelEditButton.style.display = 'none' + } + }) + } + } + + buttonWrap.appendChild(editButton) + buttonWrap.appendChild(cancelEditButton) + divWrap.appendChild(buttonWrap) + if (activeDocsEle) { hideActiveDocs() } @@ -152,23 +265,8 @@ const showNodeDocs = async function(node) { nodeDocsEleMap.set(node.id, divWrap) } -/** - * 节流函数 - */ -const throttle = function(fn, delay) { - let lastTime = 0 - return function() { - const now = Date.now() - if(now - lastTime > delay) { - fn.apply(this, arguments) - lastTime = now - } - } -} - const processMouseDown = LGraphCanvas.prototype.processMouseDown LGraphCanvas.prototype.processMouseDown = function(e) { - console.log('🚀 ~ arguments:', arguments) processMouseDown.apply(this, arguments) const { canvasX, canvasY } = e const nodes = app.graph._nodes @@ -192,10 +290,11 @@ LGraphCanvas.prototype.processMouseDown = function(e) { } } +// 注册前端插件 app.registerExtension({ name: 'Leo.NodeDocs', setup() { - console.log('🚀 ~ setup ~ app', app) + if (!app.ui.settings.getSettingValue(ENABLED_SETTING_KEY)) return // window resize重新计算所有文档的位置 window.addEventListener('resize', throttle(() => { cacheNodePositonMap.forEach((value, key) => { @@ -211,6 +310,7 @@ app.registerExtension({ }, 1000)) }, nodeCreated: function(node, app) { + if (!app.ui.settings.getSettingValue(ENABLED_SETTING_KEY)) return if(!node.doc_enabled) { let orig = node.onDrawForeground; if(!orig) @@ -245,9 +345,116 @@ app.registerExtension({ } }, loadedGraphNode(node, app) { + if (!app.ui.settings.getSettingValue(ENABLED_SETTING_KEY)) return if(!node.doc_enabled) { const orig = node.onDrawForeground; node.onDrawForeground = function (ctx) { drawDocIcon(node, orig, arguments) }; } }, -}); \ No newline at end of file +}); + +// 增加设置项目 +app.ui.settings.addSetting({ + id: ENABLED_SETTING_KEY, + name: 'comfyui nodes docs enabled', + type: 'boolean', + defaultValue: true, + onChange: (newValue, oldValue) => { + if (newValue !== oldValue && oldValue !== undefined) { + window.location.reload() + } + } +}) + +// 增加设置导出文档项目 +const settingId = "comfyui.nodes.docs.export"; +const htmlSettingId = settingId.replaceAll(".", "-"); +app.ui.settings.addSetting({ + id: 'comfyui-nodes-docs.export', + name: 'comfyui nodes docs export', + type: (name, setter, value) => { + return $el("tr", [ + $el("td", [ + $el("label", { + textContent: "comfyui nodes docs export", + for: htmlSettingId, + }), + ]), + $el("td", [ + $el("button", { + textContent: "export docs", + onclick: async () => { + const res = await fetch('/customnode/exportNodeInfo') + const blob = await res.blob() + // 把blob转成uri + const uri = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = uri + a.download = 'nodes-docs.zip' + a.click() + }, + style: { + fontSize: "14px", + display: "block", + marginTop: "5px", + }, + }), + ]), + ]); + } +}) + +// 增加设置导入文档项目 +const settingId2 = "comfyui.nodes.docs.import"; +const htmlSettingId2 = settingId2.replaceAll(".", "-"); +app.ui.settings.addSetting({ + id: 'comfyui-nodes-docs.import', + name: 'comfyui nodes docs import', + type: (name, setter, value) => { + return $el("tr", [ + $el("td", [ + $el("label", { + textContent: "comfyui nodes docs import", + for: htmlSettingId2, + }), + ]), + $el("td", [ + $el("input", { + id: htmlSettingId2, + type: "file", + onchange: async (event) => { + const file = event.target.files[0] + const formData = new FormData() + formData.append('file', file) + const res = await fetch('/customnode/importNodeInfo', { + method: 'POST', + body: formData + }) + const jsonData = await res.json() + if(jsonData.success) { + alert('导入成功') + } else { + alert('导入失败') + } + }, + }), + ]), + ]); + } +}) + +// 增加设置参与共建项目 +app.ui.settings.addSetting({ + id: 'comfyui-nodes-docs.contribute', + name: 'comfyui nodes docs contribute', + type: 'boolean', + defaultValue: true, + onChange: (newValue, oldValue) => { + if (newValue !== oldValue && oldValue !== undefined) { + updateSettingFile({ + key: 'contribute', + value: newValue + }) + } + } +}) \ No newline at end of file diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js new file mode 100644 index 00000000..51027df2 --- /dev/null +++ b/web/comfyui/utils.js @@ -0,0 +1,14 @@ + +/** + * 节流函数 + */ +export const throttle = function(fn, delay) { + let lastTime = 0 + return function() { + const now = Date.now() + if(now - lastTime > delay) { + fn.apply(this, arguments) + lastTime = now + } + } +} \ No newline at end of file