Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b05d4473f1 | ||
|
|
3c05aa1ac3 | ||
|
|
f6f40005df | ||
|
|
58ab01448b | ||
|
|
d3baa270e2 | ||
|
|
8540ac56cb | ||
|
|
5c79593deb | ||
|
|
3c3abf7367 | ||
|
|
89466f76ea | ||
|
|
4679d83ead | ||
|
|
bda1d99718 | ||
|
|
e063d1d7c2 | ||
|
|
ff16fe6b02 | ||
|
|
43b5dbf16f | ||
|
|
5533118cc5 | ||
|
|
0dd27e437d | ||
|
|
9be87ea033 | ||
|
|
aafa02374d | ||
|
|
0f45c2c126 | ||
|
|
6b14749a95 | ||
|
|
e485e348ea | ||
|
|
1179876506 | ||
|
|
e9d5a0a86a | ||
|
|
5df74ba318 | ||
|
|
06b31913e6 | ||
|
|
55eed44f33 | ||
|
|
b582406bbc | ||
|
|
47c5788704 | ||
|
|
f1ce6259b8 | ||
|
|
f3e9e2db0f | ||
|
|
d621815628 | ||
|
|
2a3e13fe00 | ||
|
|
17bcfea8c1 | ||
|
|
f4cec60c41 | ||
|
|
8fca4b5b93 | ||
|
|
2d6b794d7c | ||
|
|
3a3b6890af | ||
|
|
5f8d402b11 | ||
|
|
a8a608ea77 | ||
|
|
1d9e179277 | ||
|
|
2132f0c45c | ||
|
|
c7e0c3fed4 | ||
|
|
b9d67c6464 | ||
|
|
5ed41284a1 | ||
|
|
75eaee9cf6 | ||
|
|
4db43a2b15 | ||
|
|
5ff0f84cc8 | ||
|
|
5292e256d6 | ||
|
|
396cad23ad | ||
|
|
29582bd1ee | ||
|
|
4db975eb55 | ||
|
|
7c0c12e811 | ||
|
|
935d662c40 | ||
|
|
ecd1e12be9 | ||
|
|
c3da1e0099 | ||
|
|
43bfcb1b57 | ||
|
|
e4d38f96e2 | ||
|
|
a752f9b58a | ||
|
|
5f8de139fd | ||
|
|
3dfaf48881 | ||
|
|
2f7cbb6cee | ||
|
|
e81c2479eb | ||
|
|
eaf18ef95f | ||
|
|
af46d191c7 | ||
|
|
5e36ae7620 | ||
|
|
a96994153a | ||
|
|
ef275de172 | ||
|
|
3253d1da08 | ||
|
|
220c33c393 | ||
|
|
e398a612d7 | ||
|
|
f3406d0c3e | ||
|
|
ddbd112aff | ||
|
|
527c507859 | ||
|
|
0ea9da8a8d | ||
|
|
44c26dc102 | ||
|
|
ad709f6427 | ||
|
|
6b6b02f7d5 | ||
|
|
9cfa211158 | ||
|
|
d4d885a66f | ||
|
|
80ee9a8788 | ||
|
|
968d6333de | ||
|
|
ecac3d724c | ||
|
|
299a71696d | ||
|
|
3b893e6a37 | ||
|
|
7b517c3a45 | ||
|
|
6f8810676f | ||
|
|
e10973a63e | ||
|
|
489e320282 | ||
|
|
9844cd351d | ||
|
|
fc30789dab | ||
|
|
dbfef47eb6 | ||
|
|
014d93a765 | ||
|
|
644787856b | ||
|
|
28f51e472b | ||
|
|
28ace81e49 | ||
|
|
3cc5e5f87f | ||
|
|
8ad6cfdafd | ||
|
|
d416e2504f | ||
|
|
b3f6d71c65 | ||
|
|
9fff1ec460 | ||
|
|
04d3cd292c | ||
|
|
6837e2ef1a | ||
|
|
d044c4afac | ||
|
|
5c6f292be8 | ||
|
|
2886e2748a | ||
|
|
809202be2c | ||
|
|
6a36a5cc16 | ||
|
|
f8e302d3ea | ||
|
|
d5971af10b | ||
|
|
c92363de6c | ||
|
|
e4911519ed | ||
|
|
4f611a2003 | ||
|
|
5fd537dfdc | ||
|
|
86bccdcdce | ||
|
|
9377ea3f12 | ||
|
|
02bcc439be | ||
|
|
ebc5bc66f2 | ||
|
|
a7aaad995f | ||
|
|
2e21de1303 | ||
|
|
0207557724 | ||
|
|
a33fc3ae58 | ||
|
|
9d2373edd1 | ||
|
|
88d249bd5c | ||
|
|
ae698a2b52 | ||
|
|
14976c8376 | ||
|
|
2f2a6552ff | ||
|
|
756aaa5ded | ||
|
|
da617f6e22 | ||
|
|
69d29b1067 | ||
|
|
9fedd1e0b2 | ||
|
|
4e32d0c5fb | ||
|
|
e4cca2bfac | ||
|
|
8d3ae06d2d | ||
|
|
213074f373 | ||
|
|
77ad26ddd8 | ||
|
|
eaafab7d5f | ||
|
|
61ec0cb7f8 | ||
|
|
1a9c8c4206 | ||
|
|
f61e6fb9ff | ||
|
|
bb50aaccd9 | ||
|
|
b0b34729d4 | ||
|
|
6b17a0a0f1 | ||
|
|
91b979f09d | ||
|
|
ff0656940f | ||
|
|
3199fa4d69 | ||
|
|
9a2bfe664a | ||
|
|
ecc3c94002 | ||
|
|
984b09596c | ||
|
|
465ca416cd | ||
|
|
3c931d6d0f | ||
|
|
d7198d5c0a | ||
|
|
7a8fc57248 | ||
|
|
31f0110fb6 | ||
|
|
cb2dad6b75 | ||
|
|
3d515cf0c9 | ||
|
|
c05ce15d43 | ||
|
|
b7bd82f7b2 | ||
|
|
05c20955cd | ||
|
|
7e11037b21 | ||
|
|
568b0dfd38 | ||
|
|
163795e7fe | ||
|
|
8dbf2b4b9b | ||
|
|
8cc997cef8 | ||
|
|
13370a6e82 | ||
|
|
d0330bf140 | ||
|
|
2c2a8494d3 | ||
|
|
dabc189010 | ||
|
|
00d3ebd25c | ||
|
|
7668a5364c | ||
|
|
c7f2cba02b | ||
|
|
dea8d5a94a | ||
|
|
e12ccf6895 | ||
|
|
8615aa53a6 | ||
|
|
6419f3b880 | ||
|
|
5a9338519a | ||
|
|
cad92ad2a9 | ||
|
|
f324e9efe5 | ||
|
|
6f2a43b5c5 | ||
|
|
c08ae641ea | ||
|
|
3850db6c5a | ||
|
|
cf281ce8d5 | ||
|
|
a3770c3beb | ||
|
|
b47ce70f89 | ||
|
|
a6f6a7355a | ||
|
|
19d2af5fcc | ||
|
|
9c72bbe809 | ||
|
|
c63d6ed2f0 | ||
|
|
1f9b5ed681 | ||
|
|
72014b5491 | ||
|
|
3c74f8766e | ||
|
|
f94a3a0776 | ||
|
|
e1301aec56 | ||
|
|
480e4d6d73 | ||
|
|
300d1c26b2 | ||
|
|
a5c4bc9ee0 | ||
|
|
4b79494df5 |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2025] WWWEN8
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,105 +1,143 @@
|
||||
CCXManage是专门为SD-PPP插件开发的一个辅助节点。
|
||||
|
||||
之前用过SD-PPP节点的小伙伴都感觉非常麻烦,SD-PPP节点升级了,另外Photoshop还要比较复杂去手动去安装插件,造成SD-PPP不能及时使用最新版的Photoshop插件的功能,带来不好体验。尤其SD-PPP节点更新频率非常高,因为SD-PPP节点开发人员,为了更好完善节点,社区和群里面体验者有反馈BUG或者建议功能,第一时间去努力修复BUG或更新功能,在此给SD-PPP节点开发人员大大点赞!!!
|
||||
|
||||
CCXManag节点设置完运行一次,节点自动记忆你设置的信息,每次重启COMFYUI进行SD-PPP节点检测,SD-PPP有更新情况,自动运行节点进行photoshop插件更新。使用CCXManag节点,完全摆脱手动去Photoshop更新插件了。
|
||||
|
||||
CCXManag节点添加了可视化功能:节点运行后在COMFYUI控制台,可以查到CCXManag节点运行的信息。
|
||||
|
||||
CCXManag节点支持SD-PPP节点1.0版本和2.0版本同时运行
|
||||
|
||||
CCXManag节点支持CCX网站下载进行更新(250731新功能)
|
||||
|
||||
CCXManag节点带有检测功能,检测在SD-PPP节点有更新情况,重启COMFYUI才运行CCXManag节点进行更新插件.(250801新功能)
|
||||
|
||||
CCXManag只支持本地部署的COMFYUI,不支持云端的COMFYUI。(云端支持这是下一步开发计划。。。)
|
||||
|
||||
安装CCXManag节点准备以下条件:
|
||||
1. 已经安装Photoshop软件(版本大于24)
|
||||
2. 已经安装最新版的SD-PPP节点
|
||||
3. Photoshop安装目录找到Plug-ins,在Plug-ins文件夹里面新建两个新文件夹,分别命名为:sd-ppp_PS和sd-ppp2_PS。(之前有安装过SD-PPP插件的文件夹,请删除)准备分别复制sd-ppp_PS和sd-ppp2_PS两个路径,等待粘贴
|
||||
|
||||
安装CCXManag节点:
|
||||
|
||||
方法一:Git Clone(推荐)
|
||||
|
||||
cd ComfyUI/custom_nodes
|
||||
|
||||
git clone https://github.com/WWWEN8/ComfyUI-CCXManager.git
|
||||
|
||||
方法二:手动下载
|
||||
下载本项目的ZIP文件
|
||||
解压到 ComfyUI/custom_nodes/
|
||||
|
||||
确保文件夹结构正确
|
||||
|
||||
方法三:ComfyUI Manager
|
||||
如果已收录到ComfyUI Manager,可直接搜索"CCXManag"安装
|
||||
|
||||
CCXManag节点有两个节点:SDPPP插件更新助手1.0最新和SDPPP插件更新助手2.0最新
|
||||
|
||||
<img width="712" height="589" alt="be374a11-0423-47e4-aa9b-219df867e8f4" src="https://github.com/user-attachments/assets/dcc7d4e1-d71a-452b-ba59-612c8a5ef340" />
|
||||
|
||||
source_type选择输入:
|
||||
|
||||
1. url:(推荐)网络下载,默认即可
|
||||
|
||||
2. local_path :本地.CCX文件安装,.CCX文件一般在SD-PPP安装目录下的static文件夹里面,复制.CCX路径在target_path框里面即可,不推荐
|
||||
|
||||
例子:
|
||||
|
||||
SDPPP插件更新助手1.0最新节点:target_path框填写对应是“H:\ComfyUI\custom_nodes\sd-ppp\static\sd-ppp_PS.ccx”注意路径结尾是sd-ppp_PS.ccx
|
||||
|
||||
SDPPP插件更新助手2.0最新节点:target_path框填写对应是“H:\ComfyUI\custom_nodes\sd-ppp\static\sd-ppp2_PS.ccx”注意路径结尾是sd-ppp2_PS.ccx
|
||||
|
||||
source_path输入:
|
||||
|
||||
输入.CCX下载网站,保持默认下载的网站,不用修改
|
||||
|
||||
1. SDPPP插件更新助手1.0最新,节点默认网站:https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp_PS.ccx
|
||||
|
||||
2. SDPPP插件更新助手2.0最新,节点默认网站:https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp2_PS.ccx
|
||||
|
||||
target_path输入:
|
||||
|
||||
1.SDPPP插件更新助手1.0最新节点,输入:sd-ppp_PS文件路径,如:
|
||||
|
||||
<img width="700" height="285" alt="9408e409-d9f0-42da-a136-c4dd6da0e567" src="https://github.com/user-attachments/assets/3f478c7e-8d20-4f96-beab-9f4e9cf8e63d" />
|
||||
|
||||
2.SDPPP插件更新助手2.0最新节点,输入:sd-ppp2_PS文件路径,如:
|
||||
|
||||
<img width="745" height="234" alt="9eeecf46-a559-459f-bd41-dbde4710cee0" src="https://github.com/user-attachments/assets/0cb971ab-2548-457a-bf54-30ba40bf8930" />
|
||||
|
||||
github_repo_url 输入:
|
||||
|
||||
检测有最新更新github仓库更新,仓库网址输入。检测到SD-PPP节点一旦有更新情况下,激活重启COMFYUI运行CCXManag自动更新。没更新情况,重启不会运行CCXManag进行更新。保持默认,不要修改
|
||||
|
||||
默认SD-PPP节点仓库:https://github.com/zombieyang/sd-ppp.git
|
||||
|
||||
auto_run_on_restart选择输入:
|
||||
|
||||
enable代表开启,重启COMFYUI自动运行,开启了Photoshop插件同步更新功能。(默认)
|
||||
|
||||
disable代表禁用,重启COMFYUI节点不会运行,关闭了Photoshop插件同步更新功能。
|
||||
|
||||
enable开启后,要运行一次,才能生效,下次重启COMFYUI,Photoshop插件自动同步更新(disable禁用同理)
|
||||
|
||||
<img width="673" height="583" alt="21a79508-1014-4a2a-8aa8-2c14482e8717" src="https://github.com/user-attachments/assets/334be199-393b-4a03-95cc-32513dfd1020" />
|
||||
|
||||
运行节点后控制台,会显示运行信息如下(COMFYUI重启后也会看到控制台运行信息):
|
||||
|
||||
<img width="967" height="254" alt="e74fa9df-225a-4342-9111-ce818a7a802a" src="https://github.com/user-attachments/assets/81564e23-7cd8-4c7b-a802-295a1e9d157f" />
|
||||
|
||||
到Photoshop软件查看插件是否安装成功
|
||||
|
||||
注意事项:
|
||||
1. SDPPP插件更新助手1.0最新节点,target_path对应是“sd-ppp_PS文件路径”
|
||||
|
||||
2. SDPPP插件更新助手2.0最新节点,target_path对应是“sd-ppp2_PS文件路径”
|
||||
|
||||
3. 若安装路径改变,重新在节点设置改变后的路径,运行节点后生效
|
||||
|
||||
4. CCXManage节点有功能更新,导致失效,请按节点填写要求重新设置输入
|
||||
|
||||
5. 检查SD-PPP节点更新,需要开启代理网络才能使用;没有代理网络可以尝试local_path本地CCX文件来设置代替
|
||||
|
||||
# ComfyUI-CCXManager
|
||||
## <a href="./README.md">English</a> >> <a href="./README_ZH.md">中文版</a>
|
||||
<div align="center">
|
||||
<img width="1002" height="748" alt="a111aeda-03ba-4d81-aff4-45690be0777d" src="https://github.com/user-attachments/assets/cc730d43-ca7c-47a3-bafe-21cdc872cfae" />
|
||||
<br>
|
||||
<p><strong>SD-PPP Plugin Update Assistant - Automatically manages Photoshop-side plugin updates</strong></p>
|
||||
</div>
|
||||
|
||||
CCXManage is an auxiliary node specifically developed for the SD-PPP plugin.
|
||||
|
||||
Users who have previously used the SD-PPP node found it very troublesome. When the ComfyUI side of the SD-PPP node was upgraded, the Photoshop side required a more complex manual installation of the plugin, causing the SD-PPP to not be able to use the latest features of the Photoshop-side plugin in a timely manner, resulting in a poor experience. Especially since the SD-PPP node is updated at a very high frequency, because the SD-PPP node developers, in order to better improve the node, receive feedback on BUGs or suggested features from the community and group testers, and they work hard to fix BUGs or update features at the first opportunity. A big thumbs up to the SD-PPP node developers!!!
|
||||
|
||||
- ✅ Automatically remembers settings and restarts ComfyUI to automatically detect SD-PPP updates
|
||||
- ✅ Supports running both SD-PPP node version 1.0 and version 2.0 simultaneously
|
||||
- ✅ Supports downloading CCX from websites for updates
|
||||
- ✅ Has intelligent detection functionality, only runs when SD-PPP has updates
|
||||
- ✅ Provides visual operation information, displaying operation status in the ComfyUI console
|
||||
- ✅ Add Comfyui side automatic update SDPPP node
|
||||
- ✅ Latest feature: Automatic creation of installation directory folder function (Good news for those who haven't installed SD-PPP yet, run this workflow, and your SD-PPP will be fully automatically installed and subsequently maintained and updated)
|
||||
|
||||
> **Note:** Only supports locally deployed ComfyUI, not cloud-based ComfyUI
|
||||
|
||||
## 🛠️ Installation Requirements
|
||||
|
||||
1. Photoshop software installed (version ≥ 24)
|
||||
2. The latest version of the SD-PPP node installed on the ComfyUI side
|
||||
3. Requires proxy network support
|
||||
|
||||
## 📦 Installation Methods
|
||||
|
||||
### Method 1: Install via ComfyUI Manager
|
||||
Search for "ComfyUI-CCXManager" in ComfyUI Manager and install it
|
||||
|
||||
### Method 2: Git Clone (Recommended)
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/WWWEN8/ComfyUI-CCXManager.git
|
||||
```
|
||||
|
||||
### Method 3: Manual Installation
|
||||
1. Download the ZIP file of this project
|
||||
2. Extract it to the `ComfyUI/custom_nodes/` directory
|
||||
3. Restart ComfyUI
|
||||
|
||||
## 🚀 Usage Instructions
|
||||
|
||||
The CCXManage node has 3 nodes:
|
||||
Photoshop side automatic update SDPPP2.0/
|
||||
Photoshop side automatic update SDPPp1.0/
|
||||
Comfyui side automatic update SDPPP
|
||||
|
||||
### source_type selection input:
|
||||
|
||||
1. url: (Recommended) Network download, default is fine
|
||||
|
||||
2. local_path: Install local .CCX file, .CCX file is usually in the static folder under the SD-PPP installation directory, copy the .CCX path to the target_path box, not recommended
|
||||
|
||||
Example:
|
||||
|
||||
Photoshop side automatic update SDPPP1.0 node: Fill in "H:\ComfyUI\custom_nodes\sd-ppp\static\sd-ppp_PS.ccx" in the target_path box, note that the path ends with sd-ppp_PS.ccx
|
||||
|
||||
Photoshop side automatic update SDPPP2.0 node: Fill in "H:\ComfyUI\custom_nodes\sd-ppp\static\sd-ppp2_PS.ccx" in the target_path box, note that the path ends with sd-ppp2_PS.ccx
|
||||
|
||||
### source_path input:
|
||||
|
||||
Input the .CCX download website, keep the default download website, no need to modify
|
||||
|
||||
1. Photoshop side automatic update SDPPP1.0, node default website: https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp_PS.ccx
|
||||
|
||||
2. Photoshop side automatic update SDPPP1.0, node default website: https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp2_PS.ccx
|
||||
|
||||
### target_path input:
|
||||
|
||||
1. Photoshop side automatic update SDPPP1.0 node, input: sd-ppp_PS file path, such as:
|
||||
<img width="700" height="285" alt="1" src="https://github.com/user-attachments/assets/47077615-98b7-4e40-b40a-009653a4101d" />
|
||||
|
||||
2.Photoshop side automatic update SDPPP2.0 node, input: sd-ppp2_PS file path, as follows:
|
||||
|
||||
<img width="745" height="234" alt="2" src="https://github.com/user-attachments/assets/a88f9c69-2b0b-420e-995c-c2e1c3efb3f8" />
|
||||
|
||||
github_repo_url input:
|
||||
Detects the latest updates in GitHub repositories, enter the repository URL. Once an update is detected in the SD-PPP side node, activate and restart ComfyUI to run CCXManager for automatic updates. If there is no update, restarting will not run CCXManager for updates. Keep default, do not modify
|
||||
|
||||
Default SD-PPP node repository: https://github.com/zombieyang/sd-ppp.git
|
||||
|
||||
### auto_run_on_restart select input:
|
||||
enable means to enable, automatically runs on ComfyUI restart, enabling the Photoshop plugin synchronization update feature. (Default)
|
||||
|
||||
disable means to disable, does not run on ComfyUI restart, disabling the Photoshop plugin synchronization update feature.
|
||||
|
||||
After enabling, you need to run it once for it to take effect. The next time ComfyUI restarts, the Photoshop plugin will automatically sync updates (same for disable)
|
||||
|
||||
### Partners who have previously installed Photoshop can also set it up to run this way:
|
||||
|
||||
<img width="427" height="801" alt="EN-1" src="https://github.com/user-attachments/assets/ed4eda7d-5bbb-466f-b511-805c71464231" />
|
||||
|
||||
After running the node, the console will display the running information as follows (you can also see the console running information after ComfyUI restart):
|
||||
|
||||
<img width="992" height="199" alt="93a36210-5723-4959-bd90-ebb561810540" src="https://github.com/user-attachments/assets/cc375204-2c64-416b-91b8-b4d96b4cf710" />
|
||||
|
||||
Check in Photoshop software whether the plugin is installed successfully
|
||||
|
||||
|
||||
### Notes:
|
||||
1.SDPPP plugin update assistant 1.0 latest node, target_path corresponds to "sd-ppp_PS file path"
|
||||
|
||||
2.SDPPP plugin update assistant 2.0 latest node, target_path corresponds to "sd-ppp2_PS file path"
|
||||
|
||||
3.If the installation path changes, re-change the path in the node settings, it will take effect after running the node
|
||||
|
||||
4.If the CCXManage node has functional updates that cause it to fail, please reconfigure the inputs according to the node's requirements
|
||||
|
||||
5.To check for SD-PPP node updates, you need to enable a proxy network to use it; without a proxy network, you can try to use local_path local CCX file to set as an alternative
|
||||
|
||||
6.Choose the installation path carefully, as updates will clear the contents of files in the path. Do not enter installation paths randomly for operations to avoid accidentally deleting important files
|
||||
|
||||
### 👨💻 Developer Information
|
||||
Author: WWWEN8
|
||||
GitHub: https://github.com/WWWEN8/ComfyUI-CCXManager
|
||||
Issue feedback: Please submit an Issue in the GitHub repository
|
||||
📄 License
|
||||
This project is licensed under the MIT License - see the LICENSE file for details
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
# ComfyUI-CCXManager
|
||||
|
||||
<div align="center">
|
||||
<img width="1128" height="787" alt="786f390e-d068-43d2-8037-93c42ca2ea88" src="https://github.com/user-attachments/assets/5ded66a7-49a2-49bf-8cfb-6f49aedea4fc" />
|
||||
<br>
|
||||
<p><strong>SD-PPP插件更新助手 - 自动管理Photoshop侧插件更新</strong></p>
|
||||
</div>
|
||||
### 工作流
|
||||
(https://github.com/WWWEN8/ComfyUI-CCXManager/blob/ff16fe6b02207354b9a5d156ec5e37f2d3ee9894/workflows/SD-PPP%E6%8F%92%E4%BB%B6%E6%9B%B4%E6%96%B0%E5%8A%A9%E6%89%8B-%E6%94%AF%E6%8C%81PS%E6%93%8D%E4%BD%9C.json)
|
||||
CCXManage是专门为SD-PPP插件开发的一个辅助节点。
|
||||
|
||||
之前用过SD-PPP节点的小伙伴都感觉非常困扰,SD-PPP节点ComfyUI侧升级了,另外Photoshop侧还要比较复杂去手动去安装插件,造成SD-PPP不能及时使用最新版的Photoshop侧插件的功能,带来不好体验。尤其SD-PPP节点更新频率非常高,因为SD-PPP节点开发人员,为了更好完善节点,社区和群里面体验者有反馈BUG或者建议功能,第一时间去努力修复BUG或更新功能,在此给SD-PPP节点开发人员大大点赞!!!
|
||||
|
||||
- ✅ 自动记忆设置信息,重启ComfyUI自动检测SD-PPP更新
|
||||
- ✅ 支持SD-PPP节点1.0版本和2.0版本同时运行
|
||||
- ✅ 支持网站下载CCX进行更新
|
||||
- ✅ 具备智能检测功能,仅在SD-PPP有更新时运行.
|
||||
- ✅ 提供可视化运行信息,在ComfyUI控制台显示运行状态
|
||||
- ✅ 添加Comfyui侧自动更新SDPPP节点(新)
|
||||
- ✅ 最新功能:自动创建安装目录文件夹功能(没有安装过SD-PPP小伙伴有福了,运行此工作流,您的SD-PPP全自动安装和以后得到维护更新)
|
||||
|
||||
> **注意:** 只支持本地部署的ComfyUI,不支持云端ComfyUI.
|
||||
|
||||
## 🛠️ 安装条件
|
||||
|
||||
1. 已安装Photoshop软件(版本≥24)
|
||||
2. ComfyUI侧已安装最新版的SD-PPP节点
|
||||
3. 需要代理网络支持
|
||||
|
||||
## 📦 安装方法
|
||||
|
||||
### 方法一:通过ComfyUI Manager安装
|
||||
在ComfyUI Manager中搜索"ComfyUI-CCXManager"并安装
|
||||
|
||||
### 方法二:Git Clone(推荐)
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/WWWEN8/ComfyUI-CCXManager.git
|
||||
```
|
||||
|
||||
### 方法三:手动安装
|
||||
1. 下载本项目的ZIP文件
|
||||
2. 解压到`ComfyUI/custom_nodes/`目录下
|
||||
3. 重启ComfyUI
|
||||
|
||||
## 🚀 使用说明
|
||||
|
||||
CCXManag节点有三个节点:
|
||||
Photoshop side automatic update SDPPP2.0/
|
||||
Photoshop side automatic update SDPPp1.0/
|
||||
Comfyui side automatic update SDPPP
|
||||
|
||||
### source_type选择输入:
|
||||
|
||||
1. url:(推荐)网络下载,默认即可
|
||||
|
||||
2. local_path :本地.CCX文件安装,.CCX文件一般在SD-PPP安装目录下的static文件夹里面,复制.CCX路径在target_path框里面即可,不推荐
|
||||
|
||||
例子:
|
||||
|
||||
Photoshop side automatic update SDPPp1.0:target_path框填写对应是“H:\ComfyUI\custom_nodes\sd-ppp\static\sd-ppp_PS.ccx”注意路径结尾是sd-ppp_PS.ccx
|
||||
|
||||
Photoshop side automatic update SDPPp2.0:target_path框填写对应是“H:\ComfyUI\custom_nodes\sd-ppp\static\sd-ppp2_PS.ccx”注意路径结尾是sd-ppp2_PS.ccx
|
||||
|
||||
### source_path输入:
|
||||
|
||||
输入.CCX下载网站,保持默认下载的网站,不用修改
|
||||
|
||||
1. Photoshop side automatic update SDPPp1.0,节点默认网站:https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp_PS.ccx
|
||||
|
||||
2. Photoshop side automatic update SDPPp2.0,节点默认网站:https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp2_PS.ccx
|
||||
|
||||
### target_path输入:
|
||||
|
||||
1.Photoshop side automatic update SDPPp1.0,输入:sd-ppp_PS文件路径,如:
|
||||
|
||||
<img width="700" height="285" alt="9408e409-d9f0-42da-a136-c4dd6da0e567" src="https://github.com/user-attachments/assets/3f478c7e-8d20-4f96-beab-9f4e9cf8e63d" />
|
||||
|
||||
2.Photoshop side automatic update SDPPp2.0,输入:sd-ppp2_PS文件路径,如:
|
||||
|
||||
<img width="745" height="234" alt="9eeecf46-a559-459f-bd41-dbde4710cee0" src="https://github.com/user-attachments/assets/0cb971ab-2548-457a-bf54-30ba40bf8930" />
|
||||
|
||||
### github_repo_url 输入:
|
||||
|
||||
检测有最新更新github仓库更新,仓库网址输入。检测到SD-PPP侧节点一旦有更新情况下,激活重启ComfyUI运行CCXManag自动更新。没更新情况,重启不会运行CCXManag进行更新。保持默认,不要修改
|
||||
|
||||
默认SD-PPP节点仓库:https://github.com/zombieyang/sd-ppp.git
|
||||
|
||||
### auto_run_on_restart选择输入:
|
||||
|
||||
enable代表开启,重启ComfyUI自动运行,开启了Photoshop插件同步更新功能。(默认)
|
||||
|
||||
disable代表禁用,重启ComfyUI节点不会运行,关闭了Photoshop插件同步更新功能。
|
||||
|
||||
enable开启后,要运行一次,才能生效,下次重启ComfyUI,Photoshop插件自动同步更新(disable禁用同理)
|
||||
|
||||
### 之前安装过Photoshop侧的伙伴也可以这样设置运行:
|
||||
|
||||
<img width="429" height="658" alt="CH-1" src="https://github.com/user-attachments/assets/df1ceacb-3770-4820-99e7-7cc84838afe9" />
|
||||
|
||||
运行节点后控制台,会显示运行信息如下(ComfyUI重启后也会看到控制台运行信息)
|
||||
<img width="992" height="199" alt="93a36210-5723-4959-bd90-ebb561810540" src="https://github.com/user-attachments/assets/cc375204-2c64-416b-91b8-b4d96b4cf710" />
|
||||
到Photoshop软件查看插件是否安装成功
|
||||
|
||||
### 注意事项:
|
||||
1. SDPPP插件更新助手1.0最新节点,target_path对应是“sd-ppp_PS文件路径”
|
||||
|
||||
2. SDPPP插件更新助手2.0最新节点,target_path对应是“sd-ppp2_PS文件路径”
|
||||
|
||||
3. 若安装路径改变,重新在节点设置改变后的路径,运行节点后生效
|
||||
|
||||
4. CCXManage节点有功能更新,导致失效,请按节点填写要求重新设置输入
|
||||
|
||||
5. 检查SD-PPP节点更新,需要开启代理网络才能使用;没有代理网络可以尝试local_path本地CCX文件来设置代替
|
||||
|
||||
6. 安装路径谨慎选择,更新时会把路径的文件里面内容清空。不要随便输入安装路径进行操作,避免误删重要文件
|
||||
|
||||
## 👨💻 开发者信息
|
||||
|
||||
- 作者:WWWEN8
|
||||
- GitHub:[https://github.com/WWWEN8/ComfyUI-CCXManager](https://github.com/WWWEN8/ComfyUI-CCXManager)
|
||||
- 问题反馈:请在GitHub仓库提交Issue
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
此项目采用MIT许可证 - 详情请查看[LICENSE](LICENSE)文件
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
{
|
||||
"id": "c6dce420-1cff-4321-b4a8-441f7f193b8e",
|
||||
"revision": 0,
|
||||
"last_node_id": 20,
|
||||
"last_link_id": 17,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 19,
|
||||
"type": "ShowText|pysssss",
|
||||
"pos": [
|
||||
159.7385711669922,
|
||||
604.7866821289062
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 16
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "字符串",
|
||||
"name": "STRING",
|
||||
"shape": 6,
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-custom-scripts",
|
||||
"ver": "1.2.5",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "ShowText|pysssss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "ShowText|pysssss",
|
||||
"pos": [
|
||||
375.49298095703125,
|
||||
247.53738403320312
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "字符串",
|
||||
"name": "STRING",
|
||||
"shape": 6,
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-custom-scripts",
|
||||
"ver": "1.2.5",
|
||||
"widget_ue_connectable": {},
|
||||
"Node name for S&R": "ShowText|pysssss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "CCXManagerCopy",
|
||||
"pos": [
|
||||
-421.1217041015625,
|
||||
407.1729736328125
|
||||
],
|
||||
"size": [
|
||||
330.3197021484375,
|
||||
151.88575744628906
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "运行状态",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
16
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CCXManagerCopy",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"url",
|
||||
"https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp_PS.ccx",
|
||||
"",
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "CCXManager",
|
||||
"pos": [
|
||||
-23.904129028320312,
|
||||
83.93571472167969
|
||||
],
|
||||
"size": [
|
||||
301.0957946777344,
|
||||
246.99221801757812
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "运行状态",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
17
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CCXManager",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"url",
|
||||
"https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp2_PS.ccx",
|
||||
"",
|
||||
"enable"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
16,
|
||||
11,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
17,
|
||||
10,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9646149645000018,
|
||||
"offset": [
|
||||
521.1217041015625,
|
||||
16.064285278320312
|
||||
]
|
||||
},
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": [],
|
||||
"frontendVersion": "1.23.4"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 150 KiB |
+28
-2
@@ -1,3 +1,29 @@
|
||||
from .ccx_downloader_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
"""
|
||||
ComfyUI-CCXManager - SD-PPP插件更新助手
|
||||
专门为SD-PPP插件开发的辅助节点,自动管理和更新Photoshop侧插件
|
||||
"""
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
from .ccx_downloader_node import NODE_CLASS_MAPPINGS as CCX_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CCX_NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .auto_updater_node import NODE_CLASS_MAPPINGS as AUTO_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as AUTO_NODE_DISPLAY_NAME_MAPPINGS, auto_check_for_repo_updates
|
||||
from .py.lgutils import NODE_CLASS_MAPPINGS as CCX_GROUP_EXECUTOR_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CCX_GROUP_EXECUTOR_NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .node_version_manager import NODE_CLASS_MAPPINGS as VERSION_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as VERSION_NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# 合并节点映射
|
||||
NODE_CLASS_MAPPINGS = {**CCX_NODE_CLASS_MAPPINGS, **AUTO_NODE_CLASS_MAPPINGS, **CCX_GROUP_EXECUTOR_NODE_CLASS_MAPPINGS, **VERSION_NODE_CLASS_MAPPINGS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**CCX_NODE_DISPLAY_NAME_MAPPINGS, **AUTO_NODE_DISPLAY_NAME_MAPPINGS, **CCX_GROUP_EXECUTOR_NODE_DISPLAY_NAME_MAPPINGS, **VERSION_NODE_DISPLAY_NAME_MAPPINGS}
|
||||
|
||||
__version__ = "3.8"
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
# 版本更新记录
|
||||
# v3.8: 将Comfyui side automatic update SD-PPP节点功能合并到SDPPP1.0和SDPPP2.0节点中,移除了enable update和sdppp_version选项
|
||||
# v3.7: 优化PS侧节点,实现选择local_path时自动填充正确的ccx文件路径
|
||||
|
||||
# 可选:添加Web目录支持(如果有JS组件)
|
||||
WEB_DIRECTORY = "web"
|
||||
|
||||
# 在后台线程中运行自动更新检查(延迟5秒)
|
||||
import threading
|
||||
timer = threading.Timer(5.0, auto_check_for_repo_updates)
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import requests
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
__version__ = "2.0"
|
||||
|
||||
class GitHubRepoUpdater:
|
||||
"""GitHub仓库更新器,用于检查和更新指定的GitHub仓库"""
|
||||
def __init__(self):
|
||||
self.comfyui_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
self.custom_nodes_path = os.path.join(self.comfyui_path, "custom_nodes")
|
||||
self.updater_config_path = os.path.join(os.path.dirname(__file__), "updater_config.json")
|
||||
self.config = self.load_config()
|
||||
self.repo_cache = {}
|
||||
self.updatable_repos = []
|
||||
self.updated_count = 0
|
||||
|
||||
def load_config(self):
|
||||
"""加载更新器配置文件"""
|
||||
default_config = {
|
||||
"repos": [], # 存储要监控的仓库信息
|
||||
"auto_update_on_start": True,
|
||||
"last_check_time": "",
|
||||
"check_interval_days": 1,
|
||||
"max_workers": 5
|
||||
}
|
||||
try:
|
||||
if os.path.exists(self.updater_config_path):
|
||||
with open(self.updater_config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
with open(self.updater_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, indent=4, ensure_ascii=False)
|
||||
return default_config
|
||||
except Exception as e:
|
||||
print(f"[CCXManager Updater] 加载配置失败: {str(e)}")
|
||||
return default_config
|
||||
|
||||
def save_config(self):
|
||||
"""保存更新器配置文件"""
|
||||
try:
|
||||
with open(self.updater_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.config, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"[CCXManager Updater] 保存配置失败: {str(e)}")
|
||||
|
||||
def is_git_installed(self):
|
||||
"""检查Git是否安装"""
|
||||
try:
|
||||
subprocess.run(["git", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
return True
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def add_repo(self, repo_url, node_name=None, branch="main", auto_update=True):
|
||||
"""添加要监控的仓库"""
|
||||
if not repo_url:
|
||||
return False, "仓库URL不能为空"
|
||||
|
||||
# 如果没有提供节点名称,从URL中提取
|
||||
if not node_name:
|
||||
parsed_url = urlparse(repo_url)
|
||||
path_parts = parsed_url.path.strip('/').split('/')
|
||||
if len(path_parts) >= 2:
|
||||
node_name = path_parts[1].replace('.git', '')
|
||||
else:
|
||||
node_name = "unknown_repo"
|
||||
|
||||
# 获取当前SHA
|
||||
current_sha = ""
|
||||
try:
|
||||
current_sha = self.get_remote_sha(repo_url, branch)
|
||||
except Exception as e:
|
||||
print(f"[CCXManager Updater] 获取SHA失败: {str(e)}")
|
||||
|
||||
# 清空现有仓库列表,只保留当前设置的仓库
|
||||
self.config["repos"] = [{
|
||||
"repo_url": repo_url,
|
||||
"node_name": node_name,
|
||||
"branch": branch,
|
||||
"auto_update": auto_update,
|
||||
"last_commit_sha": current_sha,
|
||||
"last_update_time": ""
|
||||
}]
|
||||
|
||||
self.save_config()
|
||||
return True, f"成功添加仓库监控: {node_name},已自动清除旧仓库配置"
|
||||
|
||||
def get_remote_sha(self, repo_url, branch="main"):
|
||||
"""获取远程仓库的最新SHA"""
|
||||
# 检查缓存
|
||||
cache_key = f"{repo_url}:{branch}"
|
||||
if cache_key in self.repo_cache:
|
||||
cached_sha, cached_time = self.repo_cache[cache_key]
|
||||
# 缓存5分钟
|
||||
if (datetime.now() - cached_time).total_seconds() < 300:
|
||||
return cached_sha
|
||||
|
||||
# 尝试使用git命令直接获取(参考Comfy-NodeUpdater的方式)
|
||||
if self.is_git_installed():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "ls-remote", repo_url, branch],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
sha = result.stdout.split()[0]
|
||||
self.repo_cache[cache_key] = (sha, datetime.now())
|
||||
return sha
|
||||
except Exception as e:
|
||||
# 不打印详细错误信息
|
||||
pass
|
||||
|
||||
# 尝试使用GitHub API(备选方案)
|
||||
try:
|
||||
parsed_url = urlparse(repo_url)
|
||||
if parsed_url.netloc == "github.com":
|
||||
path_parts = parsed_url.path.strip('/').split('/')
|
||||
if len(path_parts) >= 2:
|
||||
owner, repo = path_parts[:2]
|
||||
# 移除仓库名称中的.git后缀
|
||||
if repo.endswith('.git'):
|
||||
repo = repo[:-4]
|
||||
api_url = f"https://api.github.com/repos/{owner}/{repo}/commits/{branch}"
|
||||
|
||||
# 添加错误处理和超时设置
|
||||
try:
|
||||
response = requests.get(api_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
sha = response.json().get("sha", "")
|
||||
if sha:
|
||||
self.repo_cache[cache_key] = (sha, datetime.now())
|
||||
return sha
|
||||
except requests.RequestException:
|
||||
# 捕获网络相关异常,不打印详细信息
|
||||
pass
|
||||
except Exception as e:
|
||||
# 不打印详细错误信息
|
||||
pass
|
||||
|
||||
# 所有方法都失败,抛出异常
|
||||
raise Exception("无法获取远程仓库的SHA")
|
||||
|
||||
def check_repo_for_update(self, repo_info):
|
||||
"""检查单个仓库是否有更新"""
|
||||
node_name = repo_info["node_name"]
|
||||
|
||||
if not repo_info["auto_update"]:
|
||||
return node_name, False, "自动更新已禁用"
|
||||
|
||||
# 检查本地仓库是否存在
|
||||
node_path = os.path.join(self.custom_nodes_path, node_name)
|
||||
git_path = os.path.join(node_path, ".git")
|
||||
|
||||
if not os.path.exists(git_path):
|
||||
# 本地没有仓库,视为需要更新
|
||||
return node_name, True, "本地仓库不存在"
|
||||
|
||||
try:
|
||||
# 尝试使用本地git命令检查更新
|
||||
try:
|
||||
# 执行git fetch,移除重复的打印信息
|
||||
fetch_result = subprocess.run(
|
||||
["git", "-C", node_path, "fetch"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if fetch_result.returncode == 0:
|
||||
# 检查本地与远程的差异
|
||||
status_result = subprocess.run(
|
||||
["git", "-C", node_path, "status", "-uno"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
has_updates = "Your branch is behind" in status_result.stdout
|
||||
return node_name, has_updates, "本地git检查成功"
|
||||
else:
|
||||
# git fetch失败,记录错误但不打印到控制台
|
||||
error_msg = f"git fetch失败"
|
||||
return node_name, False, error_msg # 立即返回错误,不继续执行
|
||||
except Exception as e:
|
||||
# 本地git操作异常,不打印详细错误到控制台
|
||||
error_msg = f"本地git操作异常"
|
||||
return node_name, False, error_msg # 立即返回错误,不继续执行
|
||||
|
||||
# 注意:以下代码在git操作失败时不会执行,因为上面已经返回了
|
||||
current_sha = self.get_remote_sha(repo_info["repo_url"], repo_info["branch"])
|
||||
has_update = current_sha != repo_info["last_commit_sha"] or repo_info["last_commit_sha"] == ""
|
||||
return node_name, has_update, current_sha
|
||||
except Exception as e:
|
||||
# 检查更新异常,不打印详细错误到控制台
|
||||
return node_name, False, "检查更新异常"
|
||||
|
||||
def update_repo(self, repo_info, force_override=False):
|
||||
"""更新单个仓库(参考Comfy-NodeUpdater的实现)"""
|
||||
node_name = repo_info["node_name"]
|
||||
node_path = os.path.join(self.custom_nodes_path, node_name)
|
||||
repo_url = repo_info["repo_url"]
|
||||
branch = repo_info["branch"]
|
||||
|
||||
try:
|
||||
# 检查目录是否存在
|
||||
if not os.path.exists(node_path):
|
||||
# 目录不存在,执行克隆
|
||||
print(f"[CCXManager Updater] 开始克隆仓库: {node_name}")
|
||||
result = subprocess.run(
|
||||
["git", "clone", "-b", branch, repo_url, node_path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False, f"克隆失败: {result.stderr}"
|
||||
else:
|
||||
# 目录存在,执行pull(参考Comfy-NodeUpdater的错误处理)
|
||||
print(f"[CCXManager Updater] 开始更新仓库: {node_name}")
|
||||
|
||||
# 先尝试直接pull
|
||||
result = subprocess.run(
|
||||
["git", "-C", node_path, "pull"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
# 检查是否是因为本地更改冲突导致的失败
|
||||
if "Your local changes to the following files would be overwritten by merge" in result.stderr or force_override:
|
||||
print(f"[CCXManager Updater] 检测到{node_name}有未提交的更改,正在自动放弃本地更改...")
|
||||
|
||||
# 尝试常规的checkout
|
||||
checkout_result = subprocess.run(
|
||||
["git", "-C", node_path, "checkout", "--", "."],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if checkout_result.returncode != 0 or force_override:
|
||||
# 如果常规checkout失败或用户选择强制覆盖,使用更强制的方法
|
||||
print(f"[CCXManager Updater] 尝试强制清理{node_name}的未跟踪文件...")
|
||||
# 清理未跟踪的文件和目录
|
||||
clean_result = subprocess.run(
|
||||
["git", "-C", node_path, "clean", "-fd"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if clean_result.returncode != 0:
|
||||
return False, f"强制清理失败: {clean_result.stderr}"
|
||||
|
||||
# 再次尝试checkout
|
||||
checkout_result = subprocess.run(
|
||||
["git", "-C", node_path, "checkout", "--", "."],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if checkout_result.returncode == 0:
|
||||
print(f"[CCXManager Updater] {node_name}本地更改已放弃,正在重新尝试更新...")
|
||||
# 再次尝试git pull
|
||||
retry_result = subprocess.run(
|
||||
["git", "-C", node_path, "pull"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if retry_result.returncode == 0:
|
||||
# 更新成功
|
||||
pass
|
||||
else:
|
||||
return False, f"放弃更改后更新仍失败: {retry_result.stderr}"
|
||||
else:
|
||||
return False, f"放弃本地更改失败: {checkout_result.stderr}"
|
||||
else:
|
||||
return False, f"更新失败: {result.stderr}"
|
||||
|
||||
# 更新SHA和时间
|
||||
current_sha = self.get_remote_sha(repo_url, branch)
|
||||
for repo in self.config["repos"]:
|
||||
if repo["node_name"] == node_name:
|
||||
repo["last_commit_sha"] = current_sha
|
||||
repo["last_update_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
break
|
||||
self.save_config()
|
||||
|
||||
return True, "更新成功"
|
||||
except Exception as e:
|
||||
return False, f"更新异常: {str(e)}"
|
||||
|
||||
def check_all_repos(self):
|
||||
"""检查所有监控的仓库是否有更新(使用线程池并发处理)"""
|
||||
if not self.is_git_installed():
|
||||
print("[CCXManager Updater] 未安装Git,请先安装Git")
|
||||
return [], "未安装Git"
|
||||
|
||||
if not self.config["repos"]:
|
||||
return [], "没有监控的仓库"
|
||||
|
||||
self.updatable_repos = []
|
||||
status_message = "检查仓库更新结果:\n"
|
||||
has_any_update = False
|
||||
has_any_disabled = False
|
||||
|
||||
# 使用线程池并发检查
|
||||
with ThreadPoolExecutor(max_workers=self.config.get("max_workers", 5)) as executor:
|
||||
results = list(executor.map(self.check_repo_for_update, self.config["repos"]))
|
||||
|
||||
for node_name, has_update, result in results:
|
||||
if has_update:
|
||||
# 查找对应的repo_info
|
||||
repo_info = next((r for r in self.config["repos"] if r["node_name"] == node_name), None)
|
||||
if repo_info:
|
||||
self.updatable_repos.append(repo_info)
|
||||
status_message += f"✅ {node_name}: 发现更新\n"
|
||||
has_any_update = True
|
||||
else:
|
||||
if isinstance(result, str):
|
||||
if result != "自动更新已禁用" and "成功" not in result:
|
||||
# 不显示详细的错误信息
|
||||
status_message += f"❌ {node_name}: {result}\n"
|
||||
elif result != "自动更新已禁用":
|
||||
status_message += f"✅ {node_name}: 已是最新版本\n"
|
||||
else:
|
||||
status_message += f"🔄 {node_name}: 自动更新已禁用\n"
|
||||
has_any_disabled = True
|
||||
else:
|
||||
status_message += f"✅ {node_name}: 已是最新版本\n"
|
||||
|
||||
# 只输出一次状态信息,避免重复
|
||||
if has_any_update:
|
||||
print("[CCXManager Updater] 发现有更新,正在更新中")
|
||||
elif has_any_disabled:
|
||||
print("[CCXManager Updater] 已经忽略更新")
|
||||
else:
|
||||
print("[CCXManager Updater] 没发现更新")
|
||||
|
||||
self.config["last_check_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.save_config()
|
||||
|
||||
return self.updatable_repos, status_message
|
||||
|
||||
def update_all_repos(self, force_override=False):
|
||||
"""更新所有有更新的仓库(使用线程池并发处理)"""
|
||||
if not self.updatable_repos:
|
||||
# 如果没有预先检查,先检查
|
||||
self.check_all_repos()
|
||||
|
||||
if not self.updatable_repos:
|
||||
print("[CCXManager Updater] 没有需要更新的仓库")
|
||||
return 0, "没有需要更新的仓库"
|
||||
|
||||
self.updated_count = 0
|
||||
status_message = "更新结果:\n"
|
||||
update_results = []
|
||||
|
||||
# 使用线程池并发更新,并传递force_override参数(参考Comfy-NodeUpdater)
|
||||
with ThreadPoolExecutor(max_workers=self.config.get("max_workers", 5)) as executor:
|
||||
results = list(executor.map(lambda repo: (repo, self.update_repo(repo, force_override)), self.updatable_repos))
|
||||
|
||||
for repo_info, (success, message) in results:
|
||||
node_name = repo_info["node_name"]
|
||||
if success:
|
||||
self.updated_count += 1
|
||||
result_msg = f"✅ {node_name}: {message}\n"
|
||||
print("[CCXManager Updater] SD-PPP节点更新成功")
|
||||
else:
|
||||
result_msg = f"❌ {node_name}: {message}\n"
|
||||
print(f"[CCXManager Updater] {result_msg.strip()}") # 打印每个节点的更新结果到控制台
|
||||
status_message += result_msg
|
||||
update_results.append(result_msg.strip())
|
||||
|
||||
return self.updated_count, status_message
|
||||
|
||||
def run_auto_update(self, force_override=False, force_update=False):
|
||||
"""执行自动更新"""
|
||||
if not self.config.get("auto_update_on_start", True):
|
||||
print("[CCXManager Updater] Comfyui侧自动更新SD-PPP[更新禁用]")
|
||||
return 0, "自动更新已禁用"
|
||||
|
||||
# 添加时间间隔检查,但仅在非强制更新模式下
|
||||
if not force_update and self.config.get("last_check_time") and self.config.get("check_interval_days", 1) > 0:
|
||||
try:
|
||||
last_check = datetime.strptime(self.config["last_check_time"], "%Y-%m-%d %H:%M:%S")
|
||||
current_time = datetime.now()
|
||||
days_since_last_check = (current_time - last_check).days
|
||||
|
||||
# 检查是否是未来时间
|
||||
if last_check > current_time:
|
||||
print(f"[CCXManager Updater] 检测到未来的检查时间,重置并执行检查")
|
||||
# 重置为当前时间
|
||||
self.config["last_check_time"] = current_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.save_config()
|
||||
elif days_since_last_check < self.config["check_interval_days"]:
|
||||
print(f"[CCXManager Updater] 距离上次检查不足{self.config['check_interval_days']}天,跳过自动更新")
|
||||
return 0, f"距离上次检查不足{self.config['check_interval_days']}天,跳过自动更新"
|
||||
except Exception as e:
|
||||
print(f"[CCXManager Updater] 时间检查出错: {str(e)}")
|
||||
# 如果时间解析失败,继续执行更新检查
|
||||
|
||||
print("[CCXManager Updater] 正在SD-PPP节点检查更新...")
|
||||
|
||||
# 确保只保留当前设置的仓库(如果有的话)
|
||||
if len(self.config["repos"]) > 1:
|
||||
print("[CCXManager Updater] 检测到多个仓库配置,仅保留当前设置的仓库")
|
||||
# 保留最后一个仓库配置
|
||||
self.config["repos"] = [self.config["repos"][-1]]
|
||||
self.save_config()
|
||||
|
||||
self.check_all_repos()
|
||||
if self.updatable_repos:
|
||||
count, message = self.update_all_repos(force_override)
|
||||
return count, message
|
||||
else:
|
||||
return 0, "没有需要更新的仓库"
|
||||
|
||||
class CCXRepoUpdaterNode:
|
||||
"""CCX仓库更新器节点,简化版本"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"repo_url": ("STRING", {"default": "", "multiline": False, "placeholder": "GitHub仓库URL,如:https://github.com/user/repo.git"}),
|
||||
"auto_update": ("BOOLEAN", {"default": True}),
|
||||
"force_override": ("BOOLEAN", {"default": False})
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("Status",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "Update of SD-PPP Plugin"
|
||||
|
||||
def process(self, repo_url, auto_update, force_override):
|
||||
updater = GitHubRepoUpdater()
|
||||
status_message = ""
|
||||
|
||||
if not repo_url:
|
||||
status_message = "错误:仓库URL不能为空"
|
||||
return (status_message,)
|
||||
|
||||
# 从URL提取节点名称
|
||||
parsed_url = urlparse(repo_url)
|
||||
path_parts = parsed_url.path.strip('/').split('/')
|
||||
if len(path_parts) >= 2:
|
||||
node_name = path_parts[1].replace('.git', '')
|
||||
else:
|
||||
node_name = "unknown_repo"
|
||||
|
||||
# 无论仓库是否存在,都调用add_repo方法以确保旧仓库被删除
|
||||
success, message = updater.add_repo(repo_url, node_name, "main", auto_update)
|
||||
if not success:
|
||||
status_message = message
|
||||
return (status_message,)
|
||||
|
||||
# 先检测更新状态
|
||||
print(f"[CCXManager Updater] Comfyui侧自动检测{node_name} 仓库的更新状态")
|
||||
repo_info = next((repo for repo in updater.config["repos"] if repo["node_name"] == node_name), None)
|
||||
if repo_info:
|
||||
node_name_result, has_update, result = updater.check_repo_for_update(repo_info)
|
||||
|
||||
if has_update and auto_update:
|
||||
# 有更新且开启自动更新,执行更新
|
||||
print(f"[CCXManager Updater] 发现{node_name} 的更新,开始更新...")
|
||||
success, update_message = updater.update_repo(repo_info, force_override)
|
||||
if success:
|
||||
status_message = f"✅ {node_name}: 更新成功 [自动更新: 开启]"
|
||||
print(f"[CCXManager Updater] {status_message}")
|
||||
else:
|
||||
status_message = f"❌ {node_name}: 更新失败 - {update_message} [自动更新: 开启]"
|
||||
print(f"[CCXManager Updater] {status_message}")
|
||||
elif has_update:
|
||||
# 有更新但未开启自动更新
|
||||
status_message = f"ℹ️ {node_name}: 发现更新,但自动更新已禁用"
|
||||
print(f"[CCXManager Updater] {status_message}")
|
||||
elif isinstance(result, str) and result != "自动更新已禁用":
|
||||
# 检查失败 - 直接使用返回的具体错误信息并添加自动更新状态
|
||||
update_status = "[自动更新: 开启]" if auto_update else "[自动更新: 禁用]"
|
||||
if result.startswith("本地git检查成功"):
|
||||
status_message = f"{node_name} 检查成功,{update_status}"
|
||||
else:
|
||||
status_message = f"❌ {node_name}: {result} {update_status}"
|
||||
print(f"[CCXManager Updater] {status_message}")
|
||||
else:
|
||||
# 已是最新版本并添加自动更新状态
|
||||
update_status = "[自动更新: 开启]" if auto_update else "[自动更新: 禁用]"
|
||||
status_message = f"✅ {node_name}: 已是最新版本 {update_status}"
|
||||
print(f"[CCXManager Updater] {status_message}")
|
||||
else:
|
||||
status_message = "错误:无法找到仓库信息"
|
||||
|
||||
return (status_message,)
|
||||
|
||||
# 全局自动更新函数
|
||||
def auto_check_for_repo_updates():
|
||||
"""ComfyUI启动时自动检查仓库更新"""
|
||||
try:
|
||||
updater = GitHubRepoUpdater()
|
||||
# 获取当前自动更新设置状态
|
||||
auto_update_status = updater.config.get("auto_update_on_start", True)
|
||||
# 在控制台显示自动更新状态
|
||||
print(f"[CCXManager Updater] Comfyui侧自动更新SD-PPP[{'启动成功' if auto_update_status else '更新禁用'}]")
|
||||
|
||||
if auto_update_status:
|
||||
# 自动更新时不使用强制覆盖,但使用强制更新模式(忽略时间间隔)
|
||||
updater.run_auto_update(False, True)
|
||||
else:
|
||||
print("[CCXManager Updater] 由于自动更新功能已禁用,跳过更新检查")
|
||||
except Exception as e:
|
||||
print(f"[CCXManager Updater] 自动更新失败: {str(e)}")
|
||||
|
||||
# 节点注册映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CCXRepoUpdaterNode": CCXRepoUpdaterNode
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CCXRepoUpdaterNode": "Comfyui side automatic update SD-PPP"
|
||||
}
|
||||
|
||||
+485
-55
@@ -7,7 +7,11 @@ import hashlib
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
__version__ = "3.8"
|
||||
# 更新说明:修复了auto_run_on_restart开关状态不同步的问题,现在日志显示会准确反映用户的实际设置
|
||||
|
||||
class CCXManagerNode:
|
||||
"""CCX管理器核心类,负责配置加载、保存和自动运行检查"""
|
||||
# 修复1: 添加配置文件名参数
|
||||
def __init__(self, config_filename="config.json"):
|
||||
self.config_path = os.path.join(os.path.dirname(__file__), config_filename)
|
||||
@@ -26,7 +30,9 @@ class CCXManagerNode:
|
||||
"auto_run_on_restart": False,
|
||||
"last_run_time": "",
|
||||
"github_repo_url": "https://github.com/zombieyang/sd-ppp.git",
|
||||
"last_commit_hash": ""
|
||||
"last_commit_hash": "",
|
||||
"version": __version__,
|
||||
"force_reinstall_on_next_restart": False
|
||||
}
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
@@ -169,27 +175,48 @@ class CCXManagerNode:
|
||||
def run(self, source_path, target_path, auto_run=False):
|
||||
"""执行CCX文件处理流程"""
|
||||
self.status = "处理中"
|
||||
self.config["source_path"] = source_path
|
||||
self.config["target_path"] = target_path
|
||||
print(f"[CCXManager] 开始处理: {source_path} -> {target_path}")
|
||||
|
||||
# 保存配置前进行路径验证
|
||||
self.config["source_path"] = source_path if source_path else ""
|
||||
self.config["target_path"] = target_path if target_path else ""
|
||||
self.config["auto_run_on_restart"] = auto_run
|
||||
self.config["last_run_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 设置下次重启强制安装标志
|
||||
if auto_run:
|
||||
self.config["force_reinstall_on_next_restart"] = True
|
||||
print(f"[CCXManager] 已设置下次重启强制安装标志")
|
||||
|
||||
self.save_config()
|
||||
|
||||
# 验证目标路径
|
||||
if not target_path:
|
||||
self.status = "错误: 目标路径未设置"
|
||||
print(f"[CCXManager] {self.status}")
|
||||
return self.status
|
||||
|
||||
# 创建目标目录(确保存在)
|
||||
try:
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
print(f"[CCXManager] 确认目标目录存在: {target_path}")
|
||||
except Exception as mkdir_error:
|
||||
self.status = f"错误: 无法创建目标目录: {str(mkdir_error)}"
|
||||
print(f"[CCXManager] {self.status}")
|
||||
return self.status
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
|
||||
# 清空目标文件夹内容
|
||||
# 清空目标文件夹内容(添加更多错误处理)
|
||||
try:
|
||||
if os.path.exists(target_path):
|
||||
for item in os.listdir(target_path):
|
||||
item_path = os.path.join(target_path, item)
|
||||
if os.path.isfile(item_path) or os.path.islink(item_path):
|
||||
os.unlink(item_path)
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
try:
|
||||
if os.path.isfile(item_path) or os.path.islink(item_path):
|
||||
os.unlink(item_path)
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
except Exception as item_error:
|
||||
print(f"[CCXManager] 警告: 无法删除{item_path}: {str(item_error)}")
|
||||
print(f"[CCXManager] 目标文件夹已清空: {target_path}")
|
||||
except Exception as e:
|
||||
self.status = f"清理目标文件夹失败: {str(e)}"
|
||||
@@ -199,7 +226,7 @@ class CCXManagerNode:
|
||||
# 处理URL下载
|
||||
local_path = source_path
|
||||
ccx_filename = None
|
||||
is_url = source_path.startswith(('http://', 'https://'))
|
||||
is_url = source_path and source_path.startswith(('http://', 'https://'))
|
||||
|
||||
if is_url:
|
||||
local_path, ccx_filename = self.download_from_url(source_path)
|
||||
@@ -207,22 +234,24 @@ class CCXManagerNode:
|
||||
return self.status
|
||||
else:
|
||||
# 验证本地文件
|
||||
if not os.path.exists(local_path):
|
||||
self.status = "错误: 源文件不存在"
|
||||
if not local_path or not os.path.exists(local_path):
|
||||
self.status = "错误: 源文件不存在或路径无效"
|
||||
print(f"[CCXManager] {self.status}: {local_path}")
|
||||
return self.status
|
||||
ccx_filename = os.path.basename(local_path)
|
||||
|
||||
# 解压文件
|
||||
if self.unzip_ccx(local_path, target_path):
|
||||
self.status = f"成功: {ccx_filename} 已解压到 {target_path}"
|
||||
print(f"[CCXManager] {self.status}")
|
||||
else:
|
||||
self.status = f"失败: 无法解压 {ccx_filename}"
|
||||
print(f"[CCXManager] {self.status}")
|
||||
|
||||
# 清理临时文件
|
||||
if is_url:
|
||||
self.clean_temp_files()
|
||||
|
||||
print(f"[CCXManager] {self.status}")
|
||||
return self.status
|
||||
|
||||
def auto_run(self):
|
||||
@@ -230,32 +259,97 @@ class CCXManagerNode:
|
||||
# 检查GitHub是否有更新
|
||||
has_update = self.check_github_update()
|
||||
auto_run_enabled = self.config.get("auto_run_on_restart", False)
|
||||
force_reinstall = self.config.get("force_reinstall_on_next_restart", False)
|
||||
|
||||
# 只有在检测到更新或启用了自动运行时才执行
|
||||
if has_update or auto_run_enabled:
|
||||
print(f"[CCXManager] 检测到{'更新' if has_update else '自动运行配置'},正在执行CCX更新...")
|
||||
# 执行条件:
|
||||
# 1. 自动运行已启用,且(检测到更新 或者 需要强制重新安装)
|
||||
if auto_run_enabled and (has_update or force_reinstall):
|
||||
print(f"[CCXManager] 自动运行已启用{', 且检测到更新' if has_update else ', 执行强制重新安装' if force_reinstall else ''},正在执行CCX更新...")
|
||||
if all([self.config.get("source_path"), self.config.get("target_path")]):
|
||||
# 运行后禁用自动运行,除非是由更新触发的
|
||||
auto_run_after = has_update # 如果是更新触发的,则保持启用状态
|
||||
self.run(self.config["source_path"], self.config["target_path"], auto_run=auto_run_after)
|
||||
# 运行后保持用户的auto_run_on_restart设置不变
|
||||
result = self.run(self.config["source_path"], self.config["target_path"], auto_run=auto_run_enabled)
|
||||
|
||||
# 执行完强制重新安装后,清除标志
|
||||
if force_reinstall:
|
||||
self.config["force_reinstall_on_next_restart"] = False
|
||||
self.save_config()
|
||||
print(f"[CCXManager] 强制重新安装完成,已清除下次重启强制安装标志")
|
||||
else:
|
||||
print(f"[CCXManager] 自动运行失败: 配置不完整")
|
||||
else:
|
||||
print(f"[CCXManager] 没有检测到更新且自动运行已禁用")
|
||||
if not auto_run_enabled:
|
||||
print(f"[CCXManager] 自动运行已禁用")
|
||||
else:
|
||||
print(f"[CCXManager] 没有检测到更新,忽略自动运行")
|
||||
|
||||
# 主节点
|
||||
def get_auto_target_path(subfolder_name):
|
||||
"""获取自动目标路径,如果存在基础目录配置则使用它并添加子文件夹"""
|
||||
config_dir_path = os.path.join(os.path.dirname(__file__), "config_dir.json")
|
||||
try:
|
||||
if os.path.exists(config_dir_path):
|
||||
with open(config_dir_path, 'r', encoding='utf-8') as f:
|
||||
dir_config = json.load(f)
|
||||
base_directory = dir_config.get("base_directory", "")
|
||||
if base_directory:
|
||||
# 确保路径以Plug-ins结尾
|
||||
normalized_base = base_directory.rstrip('\\/')
|
||||
if not normalized_base.lower().endswith('\\plug-ins') and not normalized_base.lower().endswith('/plug-ins'):
|
||||
normalized_base += '\\Plug-ins'
|
||||
# 自动添加子文件夹路径,确保路径格式一致
|
||||
target_path = os.path.normpath(os.path.join(normalized_base, subfolder_name))
|
||||
print(f"[CCXManager] 为{subfolder_name}获取目标路径: {target_path}")
|
||||
return target_path
|
||||
else:
|
||||
print(f"[CCXManager] 基础目录配置为空,无法生成目标路径")
|
||||
else:
|
||||
print(f"[CCXManager] 配置文件不存在: {config_dir_path}")
|
||||
# 尝试创建默认配置
|
||||
try:
|
||||
default_config = {
|
||||
"base_directory": "",
|
||||
"auto_run_on_restart": True,
|
||||
"last_run_time": "",
|
||||
"version": __version__
|
||||
}
|
||||
with open(config_dir_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, indent=4, ensure_ascii=False)
|
||||
print(f"[CCXManager] 已创建默认配置文件: {config_dir_path}")
|
||||
except Exception as create_error:
|
||||
print(f"[CCXManager] 创建默认配置文件失败: {str(create_error)}")
|
||||
except Exception as e:
|
||||
print(f"[CCXManager] 读取目录配置失败: {str(e)}")
|
||||
# 尝试备份并重新创建配置
|
||||
try:
|
||||
backup_path = config_dir_path + ".bak"
|
||||
if os.path.exists(config_dir_path):
|
||||
shutil.copy2(config_dir_path, backup_path)
|
||||
print(f"[CCXManager] 已备份损坏的配置文件到: {backup_path}")
|
||||
|
||||
default_config = {
|
||||
"base_directory": "",
|
||||
"auto_run_on_restart": True,
|
||||
"last_run_time": "",
|
||||
"version": __version__
|
||||
}
|
||||
with open(config_dir_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, indent=4, ensure_ascii=False)
|
||||
print(f"[CCXManager] 已重新创建配置文件: {config_dir_path}")
|
||||
except Exception as recovery_error:
|
||||
print(f"[CCXManager] 恢复配置文件失败: {str(recovery_error)}")
|
||||
return ""
|
||||
|
||||
class CCXManager:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
# 加载配置以获取保存的target_path
|
||||
# 加载配置
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
||||
default_target_path = ""
|
||||
default_github_url = "https://github.com/zombieyang/sd-ppp.git"
|
||||
|
||||
try:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
default_target_path = config.get("target_path", "")
|
||||
default_github_url = config.get("github_repo_url", default_github_url)
|
||||
except Exception as e:
|
||||
print(f"[CCXManager] 加载配置失败: {str(e)}")
|
||||
@@ -264,45 +358,120 @@ class CCXManager:
|
||||
"required": {
|
||||
"source_type": (["local_path", "url"], {"default": "url"}),
|
||||
"source_path": ("STRING", {"default": "https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp2_PS.ccx", "placeholder": "本地文件路径或URL"}),
|
||||
"target_path": ("STRING", {"default": default_target_path, "placeholder": "目标解压路径,例如:i:/311/ComfyUI/custom_nodes"}),
|
||||
"github_repo_url": ("STRING", {"default": default_github_url, "placeholder": "GitHub仓库URL,留空禁用更新检测"}),
|
||||
"auto_run_on_restart": (["enable", "disable"], {"default": "enable"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("运行状态",)
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("Status", "Generated_Path")
|
||||
FUNCTION = "process_ccx"
|
||||
CATEGORY = "SD-PPP插件更新"
|
||||
TITLE = "SDPPP插件更新助手2.0最新"
|
||||
CATEGORY = "Update of SD-PPP Plugin"
|
||||
TITLE = "Photoshop side automatic update SDPPP2.0"
|
||||
|
||||
def __init__(self):
|
||||
self.manager = CCXManagerNode()
|
||||
# 初始化SDPPP路径管理器的配置
|
||||
self.config_sdppp_path = os.path.join(os.path.dirname(__file__), "config_sdppp_path.json")
|
||||
|
||||
def process_ccx(self, source_type, source_path, target_path, github_repo_url, auto_run_on_restart):
|
||||
def _get_custom_nodes_path(self):
|
||||
"""自动获取custom_nodes路径"""
|
||||
# 获取当前文件所在目录的父目录(即custom_nodes目录)
|
||||
current_dir = os.path.dirname(__file__)
|
||||
# 获取父目录,即custom_nodes目录
|
||||
custom_nodes_path = os.path.dirname(current_dir)
|
||||
|
||||
print(f"[CCXManager] 自动识别的custom_nodes路径: {custom_nodes_path}")
|
||||
return custom_nodes_path
|
||||
|
||||
def _save_sdppp_path_config(self, enable_update=True):
|
||||
"""保存SDPPP路径配置"""
|
||||
try:
|
||||
config = {
|
||||
"enable_update": enable_update,
|
||||
"last_run_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"version": __version__
|
||||
}
|
||||
with open(self.config_sdppp_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||
print(f"[CCXManager] SDPPP路径配置已保存到: {self.config_sdppp_path}")
|
||||
except Exception as e:
|
||||
print(f"[CCXManager] 保存SDPPP路径配置失败: {str(e)}")
|
||||
|
||||
def process_ccx(self, source_type, source_path, github_repo_url, auto_run_on_restart):
|
||||
auto_run = auto_run_on_restart == "enable"
|
||||
# 更新GitHub仓库URL配置
|
||||
self.manager.config["github_repo_url"] = github_repo_url
|
||||
self.manager.save_config()
|
||||
# 根据源类型处理
|
||||
if source_type == "url" and not source_path.startswith(('http://', 'https://')):
|
||||
return ("错误: URL格式不正确",)
|
||||
status = self.manager.run(source_path, target_path, auto_run)
|
||||
return (status,)
|
||||
|
||||
# 自动获取目标路径
|
||||
target_path = get_auto_target_path("sd-ppp2_PS")
|
||||
if not target_path:
|
||||
return ("错误: 无法获取目标路径,请先使用Create SD-PPP installation directory节点设置基础路径", "")
|
||||
|
||||
try:
|
||||
# 如果选择local_path,执行Comfyui side节点的功能
|
||||
generated_path = ""
|
||||
if source_type == "local_path":
|
||||
# 保存SDPPP路径配置
|
||||
self._save_sdppp_path_config(enable_update=True)
|
||||
|
||||
# 自动获取custom_nodes路径
|
||||
custom_nodes_path = self._get_custom_nodes_path()
|
||||
|
||||
# 验证custom_nodes_path是否存在
|
||||
if not os.path.exists(custom_nodes_path):
|
||||
return (f"错误: 无法识别有效的custom_nodes目录: {custom_nodes_path}", "")
|
||||
|
||||
# 生成对应的CCX文件路径
|
||||
generated_path = os.path.join(custom_nodes_path, "sd-ppp", "static", "sd-ppp2_PS.ccx")
|
||||
|
||||
# 输出运行信息
|
||||
print(f"[CCXManager] 运行信息:")
|
||||
print(f"[CCXManager] - 版本: SDPPP2.0")
|
||||
print(f"[CCXManager] - 自动识别的custom_nodes路径: {custom_nodes_path}")
|
||||
print(f"[CCXManager] - 生成的CCX路径: {generated_path}")
|
||||
print(f"[CCXManager] - 更新开关: {'开启' if auto_run else '关闭'}")
|
||||
print(f"[CCXManager] - 目标节点: CCXManager")
|
||||
|
||||
# 自动设置source_path
|
||||
source_path = generated_path
|
||||
print(f"[CCXManager] SDPPP2.0节点自动填充路径: {source_path}")
|
||||
|
||||
# 确保保存源路径和目标路径到配置中,防止下一次运行时丢失
|
||||
self.manager.config["source_path"] = source_path
|
||||
self.manager.config["target_path"] = target_path
|
||||
self.manager.save_config()
|
||||
|
||||
if source_type == "url" and not source_path.startswith(("http://", "https://")):
|
||||
return ("错误: URL格式不正确", "")
|
||||
|
||||
# 验证目标路径是否存在,如果不存在则尝试创建
|
||||
if not os.path.exists(target_path):
|
||||
try:
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
print(f"[CCXManager] 已创建目标目录: {target_path}")
|
||||
except Exception as mkdir_error:
|
||||
return (f"错误: 无法创建目标目录: {str(mkdir_error)}", "")
|
||||
|
||||
status = self.manager.run(source_path, target_path, auto_run)
|
||||
return (status, generated_path)
|
||||
except Exception as e:
|
||||
error_msg = f"处理失败: {str(e)}"
|
||||
print(f"[CCXManager] {error_msg}")
|
||||
return (error_msg, "")
|
||||
|
||||
# 分身节点
|
||||
class CCXManagerCopy:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
# 加载配置以获取保存的target_path
|
||||
# 加载配置
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config_copy.json")
|
||||
default_target_path = ""
|
||||
default_github_url = "https://github.com/zombieyang/sd-ppp.git"
|
||||
|
||||
try:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
default_target_path = config.get("target_path", "")
|
||||
default_github_url = config.get("github_repo_url", default_github_url)
|
||||
except Exception as e:
|
||||
print(f"[CCXManagerCopy] 加载配置失败: {str(e)}")
|
||||
@@ -311,48 +480,309 @@ class CCXManagerCopy:
|
||||
"required": {
|
||||
"source_type": (["local_path", "url"], {"default": "url"}),
|
||||
"source_path": ("STRING", {"default": "https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp_PS.ccx", "placeholder": "本地文件路径或URL"}),
|
||||
"target_path": ("STRING", {"default": default_target_path, "placeholder": "目标解压路径,例如:i:/311/ComfyUI/custom_nodes"}),
|
||||
"github_repo_url": ("STRING", {"default": default_github_url, "placeholder": "GitHub仓库URL,留空禁用更新检测"}),
|
||||
"auto_run_on_restart": (["enable", "disable"], {"default": "enable"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("运行状态",)
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("Status", "Generated_Path")
|
||||
FUNCTION = "process_ccx"
|
||||
CATEGORY = "SD-PPP插件更新"
|
||||
TITLE = "SDPPP插件更新助手1.0最新"
|
||||
CATEGORY = "Update of SD-PPP Plugin"
|
||||
TITLE = "Photoshop side automatic update SDPPP1.0"
|
||||
|
||||
def __init__(self):
|
||||
# 使用独立配置文件
|
||||
self.manager = CCXManagerNode(config_filename="config_copy.json")
|
||||
# 初始化SDPPP路径管理器的配置
|
||||
self.config_sdppp_path = os.path.join(os.path.dirname(__file__), "config_sdppp_path.json")
|
||||
|
||||
def process_ccx(self, source_type, source_path, target_path, github_repo_url, auto_run_on_restart):
|
||||
def _get_custom_nodes_path(self):
|
||||
"""自动获取custom_nodes路径"""
|
||||
# 获取当前文件所在目录的父目录(即custom_nodes目录)
|
||||
current_dir = os.path.dirname(__file__)
|
||||
# 获取父目录,即custom_nodes目录
|
||||
custom_nodes_path = os.path.dirname(current_dir)
|
||||
|
||||
print(f"[CCXManagerCopy] 自动识别的custom_nodes路径: {custom_nodes_path}")
|
||||
return custom_nodes_path
|
||||
|
||||
def _save_sdppp_path_config(self, enable_update=True):
|
||||
"""保存SDPPP路径配置"""
|
||||
try:
|
||||
config = {
|
||||
"enable_update": enable_update,
|
||||
"last_run_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"version": __version__
|
||||
}
|
||||
with open(self.config_sdppp_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||
print(f"[CCXManagerCopy] SDPPP路径配置已保存到: {self.config_sdppp_path}")
|
||||
except Exception as e:
|
||||
print(f"[CCXManagerCopy] 保存SDPPP路径配置失败: {str(e)}")
|
||||
|
||||
def process_ccx(self, source_type, source_path, github_repo_url, auto_run_on_restart):
|
||||
auto_run = auto_run_on_restart == "enable"
|
||||
# 更新GitHub仓库URL配置
|
||||
self.manager.config["github_repo_url"] = github_repo_url
|
||||
self.manager.save_config()
|
||||
if source_type == "url" and not source_path.startswith(("http://", "https://")):
|
||||
return ("错误: URL格式不正确",)
|
||||
status = self.manager.run(source_path, target_path, auto_run)
|
||||
return (status,)
|
||||
|
||||
# 自动获取目标路径
|
||||
target_path = get_auto_target_path("sd-ppp_PS")
|
||||
if not target_path:
|
||||
return ("错误: 无法获取目标路径,请先使用Create SD-PPP installation directory节点设置基础路径", "")
|
||||
|
||||
try:
|
||||
# 如果选择local_path,执行Comfyui side节点的功能
|
||||
generated_path = ""
|
||||
if source_type == "local_path":
|
||||
# 保存SDPPP路径配置
|
||||
self._save_sdppp_path_config(enable_update=True)
|
||||
|
||||
# 自动获取custom_nodes路径
|
||||
custom_nodes_path = self._get_custom_nodes_path()
|
||||
|
||||
# 验证custom_nodes_path是否存在
|
||||
if not os.path.exists(custom_nodes_path):
|
||||
return (f"错误: 无法识别有效的custom_nodes目录: {custom_nodes_path}", "")
|
||||
|
||||
# 生成对应的CCX文件路径
|
||||
generated_path = os.path.join(custom_nodes_path, "sd-ppp", "static", "sd-ppp_PS.ccx")
|
||||
|
||||
# 输出运行信息
|
||||
print(f"[CCXManagerCopy] 运行信息:")
|
||||
print(f"[CCXManagerCopy] - 版本: SDPPP1.0")
|
||||
print(f"[CCXManagerCopy] - 自动识别的custom_nodes路径: {custom_nodes_path}")
|
||||
print(f"[CCXManagerCopy] - 生成的CCX路径: {generated_path}")
|
||||
print(f"[CCXManagerCopy] - 更新开关: {'开启' if auto_run else '关闭'}")
|
||||
print(f"[CCXManagerCopy] - 目标节点: CCXManagerCopy")
|
||||
|
||||
# 自动设置source_path
|
||||
source_path = generated_path
|
||||
print(f"[CCXManagerCopy] SDPPP1.0节点自动填充路径: {source_path}")
|
||||
|
||||
# 确保保存源路径和目标路径到配置中,防止下一次运行时丢失
|
||||
self.manager.config["source_path"] = source_path
|
||||
self.manager.config["target_path"] = target_path
|
||||
self.manager.save_config()
|
||||
|
||||
if source_type == "url" and not source_path.startswith(("http://", "https://")):
|
||||
return ("错误: URL格式不正确", "")
|
||||
|
||||
# 验证目标路径是否存在,如果不存在则尝试创建
|
||||
if not os.path.exists(target_path):
|
||||
try:
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
print(f"[CCXManagerCopy] 已创建目标目录: {target_path}")
|
||||
except Exception as mkdir_error:
|
||||
return (f"错误: 无法创建目标目录: {str(mkdir_error)}", "")
|
||||
|
||||
status = self.manager.run(source_path, target_path, auto_run)
|
||||
return (status, generated_path)
|
||||
except Exception as e:
|
||||
error_msg = f"处理失败: {str(e)}"
|
||||
print(f"[CCXManagerCopy] {error_msg}")
|
||||
return (error_msg, "")
|
||||
|
||||
# 创建安装目录节点
|
||||
class CreateSDPPPInstallationDirectory:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
# 加载配置以获取保存的base_directory
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config_dir.json")
|
||||
default_base_directory = ""
|
||||
try:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
default_base_directory = config.get("base_directory", "")
|
||||
except Exception as e:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 加载配置失败: {str(e)}")
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"base_directory": ("STRING", {"default": default_base_directory, "placeholder": "基础目录路径,例如:C:/Program Files/Adobe/Adobe Photoshop 2025"}),
|
||||
"auto_run_on_restart": (["enable", "disable"], {"default": "enable"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("Status",)
|
||||
FUNCTION = "create_directories"
|
||||
CATEGORY = "Update of SD-PPP Plugin"
|
||||
TITLE = "Create SD-PPP installation directory"
|
||||
|
||||
def __init__(self):
|
||||
self.config_path = os.path.join(os.path.dirname(__file__), "config_dir.json")
|
||||
self.config = self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置文件,如果不存在则创建默认配置"""
|
||||
default_config = {
|
||||
"base_directory": "",
|
||||
"auto_run_on_restart": True,
|
||||
"last_run_time": "",
|
||||
"version": __version__
|
||||
}
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, indent=4, ensure_ascii=False)
|
||||
return default_config
|
||||
except Exception as e:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 加载配置失败: {str(e)}")
|
||||
return default_config
|
||||
|
||||
def _save_config(self):
|
||||
"""保存配置文件"""
|
||||
try:
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.config, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 保存配置失败: {str(e)}")
|
||||
|
||||
def create_directories(self, base_directory, auto_run_on_restart):
|
||||
try:
|
||||
# 确保路径以\Plug-ins结尾
|
||||
normalized_base_directory = base_directory.rstrip('\\/')
|
||||
if not normalized_base_directory.lower().endswith('\\plug-ins') and not normalized_base_directory.lower().endswith('/plug-ins'):
|
||||
normalized_base_directory += '\\Plug-ins'
|
||||
|
||||
# 更新配置
|
||||
self.config["base_directory"] = normalized_base_directory
|
||||
self.config["auto_run_on_restart"] = auto_run_on_restart == "enable"
|
||||
self.config["last_run_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self._save_config()
|
||||
|
||||
# 验证基础目录是否存在
|
||||
if not normalized_base_directory:
|
||||
return ("错误: 基础目录路径未设置",)
|
||||
|
||||
if not os.path.exists(normalized_base_directory):
|
||||
return (f"错误: 基础目录不存在: {normalized_base_directory}",)
|
||||
|
||||
# 创建两个目标文件夹
|
||||
folders_to_create = ["sd-ppp_PS", "sd-ppp2_PS"]
|
||||
created_folders = []
|
||||
|
||||
for folder_name in folders_to_create:
|
||||
folder_path = os.path.join(normalized_base_directory, folder_name)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
created_folders.append(folder_path)
|
||||
print(f"[CreateSDPPPInstallationDirectory] 已创建文件夹: {folder_path}")
|
||||
|
||||
status_msg = f"成功: 已在 {normalized_base_directory} 下创建文件夹: {', '.join(folders_to_create)}"
|
||||
print(f"[CreateSDPPPInstallationDirectory] {status_msg}")
|
||||
return (status_msg,)
|
||||
except Exception as e:
|
||||
error_msg = f"创建文件夹失败: {str(e)}"
|
||||
print(f"[CreateSDPPPInstallationDirectory] {error_msg}")
|
||||
return (error_msg,)
|
||||
|
||||
def auto_run(self):
|
||||
"""启动时自动运行目录创建功能"""
|
||||
auto_run_enabled = self.config.get("auto_run_on_restart", True)
|
||||
base_directory = self.config.get("base_directory", "")
|
||||
|
||||
if auto_run_enabled and base_directory:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 检测到自动运行配置,正在创建目录...")
|
||||
# 直接调用创建目录的逻辑
|
||||
try:
|
||||
if os.path.exists(base_directory):
|
||||
# 创建两个目标文件夹
|
||||
folders_to_create = ["sd-ppp_PS", "sd-ppp2_PS"]
|
||||
for folder_name in folders_to_create:
|
||||
folder_path = os.path.join(base_directory, folder_name)
|
||||
if os.path.exists(folder_path):
|
||||
print(f"[CreateSDPPPInstallationDirectory] {folder_name}文件夹已存在")
|
||||
else:
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
print(f"[CreateSDPPPInstallationDirectory] 自动运行: 已创建文件夹: {folder_path}")
|
||||
print(f"[CreateSDPPPInstallationDirectory] 自动运行: 目录创建成功")
|
||||
else:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 自动运行失败: 基础目录不存在: {base_directory}")
|
||||
except Exception as e:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 自动运行失败: {str(e)}")
|
||||
else:
|
||||
print(f"[CreateSDPPPInstallationDirectory] 自动运行已禁用或基础目录未设置")
|
||||
|
||||
# 已将Comfyui side automatic update SD-PPP节点的功能合并到SDPPP1.0和SDPPP2.0节点中
|
||||
|
||||
# 修改现有节点以支持自动填充路径
|
||||
# 修改CCXManager(SDPPP2.0)的process_ccx方法
|
||||
original_process_ccx = CCXManager.process_ccx
|
||||
def modified_process_ccx_20(self, source_type, source_path, github_repo_url, auto_run_on_restart):
|
||||
# 如果选择local_path,自动获取正确的ccx文件路径
|
||||
if source_type == "local_path":
|
||||
try:
|
||||
# 自动获取custom_nodes路径(当前文件所在目录的父目录)
|
||||
current_dir = os.path.dirname(__file__)
|
||||
custom_nodes_path = os.path.dirname(current_dir)
|
||||
source_path = os.path.join(custom_nodes_path, "sd-ppp", "static", "sd-ppp2_PS.ccx")
|
||||
print(f"[CCXManager] SDPPP2.0节点自动填充路径: {source_path}")
|
||||
except Exception as e:
|
||||
print(f"[CCXManager] 自动获取路径失败: {str(e)}")
|
||||
|
||||
# 调用原始方法
|
||||
return original_process_ccx(self, source_type, source_path, github_repo_url, auto_run_on_restart)
|
||||
|
||||
# 修改CCXManagerCopy(SDPPP1.0)的process_ccx方法
|
||||
original_process_ccx_copy = CCXManagerCopy.process_ccx
|
||||
def modified_process_ccx_10(self, source_type, source_path, github_repo_url, auto_run_on_restart):
|
||||
# 如果选择local_path且source_path为空,自动获取路径
|
||||
if source_type == "local_path" and not source_path:
|
||||
try:
|
||||
# 自动获取custom_nodes路径(当前文件所在目录的父目录)
|
||||
current_dir = os.path.dirname(__file__)
|
||||
custom_nodes_path = os.path.dirname(current_dir)
|
||||
source_path = os.path.join(custom_nodes_path, "sd-ppp", "static", "sd-ppp_PS.ccx")
|
||||
print(f"[CCXManagerCopy] 自动识别并填充local_path: {source_path}")
|
||||
except Exception as e:
|
||||
print(f"[CCXManagerCopy] 自动获取路径失败: {str(e)}")
|
||||
|
||||
# 调用原始方法
|
||||
return original_process_ccx_copy(self, source_type, source_path, github_repo_url, auto_run_on_restart)
|
||||
|
||||
# 应用修改
|
||||
CCXManager.process_ccx = modified_process_ccx_20
|
||||
CCXManagerCopy.process_ccx = modified_process_ccx_10
|
||||
|
||||
# 已移除工作流状态跟踪,因为Comfyui side节点功能已合并到SDPPP1.0和SDPPP2.0节点中
|
||||
|
||||
# 已移除节点状态跟踪相关的修改
|
||||
|
||||
# 已移除依赖检查相关的修改,因为功能已合并
|
||||
|
||||
# 节点注册
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CCXManager": CCXManager,
|
||||
"CCXManagerCopy": CCXManagerCopy
|
||||
"CCXManagerCopy": CCXManagerCopy,
|
||||
"CreateSDPPPInstallationDirectory": CreateSDPPPInstallationDirectory
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CCXManager": "SDPPP插件更新助手2.0最新",
|
||||
"CCXManagerCopy": "SDPPP插件更新助手1.0最新"
|
||||
"CCXManager": "Photoshop side automatic update SDPPP2.0",
|
||||
"CCXManagerCopy": "Photoshop side automatic update SDPPP1.0",
|
||||
"CreateSDPPPInstallationDirectory": "Create SD-PPP installation directory"
|
||||
}
|
||||
|
||||
# 程序启动时自动检查并运行
|
||||
print(f"[CCXManager] SDPPP插件更新助手节点已加载")
|
||||
# 主节点自动运行检查
|
||||
print(f"[CCXManager] Photoshop侧自动更新SD-PPP节点已加载 (版本: {__version__})")
|
||||
# 首先运行目录创建节点(优先级最高)
|
||||
dir_creator = CreateSDPPPInstallationDirectory()
|
||||
dir_creator.auto_run()
|
||||
|
||||
# 然后运行其他节点的自动检查
|
||||
# 主节点(SDPPP2.0)自动运行检查
|
||||
manager = CCXManagerNode()
|
||||
manager.auto_run()
|
||||
|
||||
# SDPPP1.0自动运行检查(使用独立配置文件)
|
||||
manager_copy = CCXManagerNode(config_filename="config_copy.json")
|
||||
manager_copy.auto_run()
|
||||
# 克隆节点自动运行检查
|
||||
manager_copy = CCXManagerNode(config_filename="config_copy.json")
|
||||
manager_copy.auto_run()
|
||||
manager_copy.auto_run()
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import os
|
||||
import http.server
|
||||
import socketserver
|
||||
import json
|
||||
import shutil
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
class LocalProxyHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parsed_path = urlparse(self.path)
|
||||
query_params = parse_qs(parsed_path.query)
|
||||
|
||||
# 处理ping请求,用于测试连接
|
||||
if parsed_path.path == '/ping':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {'status': 'success', 'message': 'Local proxy is running'}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
return
|
||||
|
||||
# 处理文件传输请求
|
||||
elif parsed_path.path == '/transfer_ccx':
|
||||
try:
|
||||
# 获取参数
|
||||
source_path = query_params.get('source_path', [None])[0]
|
||||
target_path = query_params.get('target_path', [None])[0]
|
||||
|
||||
if not source_path or not target_path:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {'status': 'error', 'message': 'Missing source_path or target_path'}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
return
|
||||
|
||||
# 检查源文件是否存在
|
||||
if not os.path.exists(source_path):
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {'status': 'error', 'message': f'Source file not found: {source_path}'}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
return
|
||||
|
||||
# 确保目标目录存在
|
||||
target_dir = os.path.dirname(target_path)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
# 复制文件
|
||||
shutil.copy2(source_path, target_path)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {'status': 'success', 'message': f'File transferred to {target_path}'}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {'status': 'error', 'message': str(e)}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
return
|
||||
|
||||
# 未知请求路径
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
response = {'status': 'error', 'message': 'Unknown endpoint'}
|
||||
self.wfile.write(json.dumps(response).encode('utf-8'))
|
||||
return
|
||||
|
||||
def run_proxy_server(port=8000):
|
||||
"""启动本地代理服务器"""
|
||||
server_address = ('', port)
|
||||
httpd = socketserver.TCPServer(server_address, LocalProxyHandler)
|
||||
print(f'[LocalProxy] 服务器启动在端口 {port}')
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print('[LocalProxy] 服务器关闭')
|
||||
httpd.server_close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_proxy_server()
|
||||
@@ -0,0 +1,306 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
class NodeVersionController:
|
||||
"""节点版本控制器,用于管理节点的版本切换功能"""
|
||||
def __init__(self):
|
||||
self.comfyui_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
self.custom_nodes_path = os.path.join(self.comfyui_path, "custom_nodes")
|
||||
|
||||
def is_git_installed(self):
|
||||
"""检查Git是否安装"""
|
||||
try:
|
||||
subprocess.run(["git", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
return True
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def get_node_commit_history(self, node_name, max_count=None):
|
||||
"""获取节点的提交历史"""
|
||||
try:
|
||||
node_path = os.path.join(self.custom_nodes_path, node_name)
|
||||
|
||||
# 检查节点目录是否存在
|
||||
if not os.path.exists(node_path):
|
||||
return False, f"Node directory not found: {node_name}"
|
||||
|
||||
# 检查是否是Git仓库
|
||||
if not os.path.exists(os.path.join(node_path, ".git")):
|
||||
return False, f"Node is not a Git repository: {node_name}"
|
||||
|
||||
# 获取当前分支
|
||||
branch_result = subprocess.run(
|
||||
["git", "-C", node_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "(detached HEAD)"
|
||||
|
||||
# 获取提交历史
|
||||
log_cmd = ["git", "-C", node_path, "log", "--pretty=format:%h|%an|%ad|%s", "--date=short"]
|
||||
if max_count:
|
||||
log_cmd.extend(["-n", str(max_count)])
|
||||
|
||||
result = subprocess.run(
|
||||
log_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return False, f"Failed to get commit history: {result.stderr}"
|
||||
|
||||
# 解析提交历史
|
||||
commits = []
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if line:
|
||||
parts = line.split("|", 3)
|
||||
if len(parts) >= 4:
|
||||
commits.append({
|
||||
"hash": parts[0],
|
||||
"author": parts[1],
|
||||
"date": parts[2],
|
||||
"message": parts[3]
|
||||
})
|
||||
|
||||
return True, {"branch": current_branch, "commits": commits}
|
||||
except Exception as e:
|
||||
return False, f"Error getting commit history: {str(e)}"
|
||||
|
||||
def switch_node_version(self, node_name, version_offset, auto_add_to_ignore=True):
|
||||
"""切换节点版本
|
||||
|
||||
Args:
|
||||
node_name: Node name
|
||||
version_offset: Version offset, 1 means latest version, negative numbers mean going back from latest version
|
||||
"""
|
||||
try:
|
||||
node_path = os.path.join(self.custom_nodes_path, node_name)
|
||||
|
||||
# 检查节点目录是否存在
|
||||
if not os.path.exists(node_path):
|
||||
return False, f"Node directory not found: {node_name}"
|
||||
|
||||
# 检查是否是Git仓库
|
||||
if not os.path.exists(os.path.join(node_path, ".git")):
|
||||
return False, f"Node is not a Git repository: {node_name}"
|
||||
|
||||
# 获取当前分支
|
||||
branch_result = subprocess.run(
|
||||
["git", "-C", node_path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "(detached HEAD)"
|
||||
|
||||
# 如果当前是游离状态,使用默认分支
|
||||
if current_branch == "HEAD" or current_branch == "(detached HEAD)":
|
||||
# 尝试获取默认分支
|
||||
default_branch_result = subprocess.run(
|
||||
["git", "-C", node_path, "symbolic-ref", "refs/remotes/origin/HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if default_branch_result.returncode == 0:
|
||||
# 从输出如"refs/remotes/origin/main"中提取"main"
|
||||
current_branch = default_branch_result.stdout.strip().split("/")[-1]
|
||||
# 切换到默认分支
|
||||
subprocess.run(["git", "-C", node_path, "checkout", current_branch], check=False)
|
||||
else:
|
||||
# 如果无法获取默认分支,创建一个临时分支
|
||||
current_branch = f"temp_branch_{int(datetime.now().timestamp())}"
|
||||
subprocess.run(["git", "-C", node_path, "checkout", "-b", current_branch], check=False)
|
||||
|
||||
# 检查是否有未提交的更改
|
||||
status_result = subprocess.run(
|
||||
["git", "-C", node_path, "status", "--porcelain"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
has_stashed_changes = False
|
||||
|
||||
# 如果有未提交的更改,暂存它们
|
||||
if status_result.stdout.strip():
|
||||
subprocess.run(["git", "-C", node_path, "stash", "push", "-m", "Auto-stash before version switch"], check=False)
|
||||
has_stashed_changes = True
|
||||
|
||||
# 新的版本切换逻辑:
|
||||
# 1表示最新版本,负数表示从最新版本开始后退相应数量的版本
|
||||
if version_offset != 1 and version_offset >= 0:
|
||||
return False, "Error: Version offset can only be 1 (latest) or negative numbers (e.g., -2, -3)"
|
||||
|
||||
# 无论选择什么版本,都先拉取最新代码以确保有完整的提交历史
|
||||
print(f"[NodeVersionController] Pulling latest code for node {node_name}...")
|
||||
fetch_result = subprocess.run(
|
||||
["git", "-C", node_path, "fetch"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if fetch_result.returncode != 0:
|
||||
print(f"[NodeVersionController] Failed to pull code: {fetch_result.stderr}")
|
||||
|
||||
# 尝试合并最新代码
|
||||
pull_result = subprocess.run(
|
||||
["git", "-C", node_path, "pull"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if pull_result.returncode != 0:
|
||||
print(f"[NodeVersionController] Failed to merge latest code, using local latest commit: {pull_result.stderr}")
|
||||
|
||||
# 确定目标提交
|
||||
if version_offset == 1:
|
||||
target_commit = "HEAD"
|
||||
print(f"[NodeVersionController] Switching node {node_name} to latest version...")
|
||||
else: # 负数偏移量
|
||||
target_commit = f"HEAD~{abs(version_offset)}"
|
||||
print(f"[NodeVersionController] Going back {abs(version_offset)} versions from latest")
|
||||
|
||||
# 创建临时分支指向目标提交
|
||||
temp_branch = f"temp_version_switch_{int(datetime.now().timestamp())}"
|
||||
checkout_result = subprocess.run(
|
||||
["git", "-C", node_path, "checkout", "-b", temp_branch, target_commit],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if checkout_result.returncode != 0:
|
||||
# 如果切换失败,恢复stash(如果有)
|
||||
if has_stashed_changes:
|
||||
subprocess.run(["git", "-C", node_path, "stash", "pop"], check=False)
|
||||
return False, f"Failed to switch to target commit: {checkout_result.stderr}"
|
||||
|
||||
# 切换回原来的分支
|
||||
subprocess.run(["git", "-C", node_path, "checkout", current_branch], check=False)
|
||||
|
||||
# 强制将原来的分支重置到临时分支的位置
|
||||
reset_result = subprocess.run(
|
||||
["git", "-C", node_path, "reset", "--hard", temp_branch],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# 删除临时分支
|
||||
subprocess.run(["git", "-C", node_path, "branch", "-D", temp_branch], check=False)
|
||||
|
||||
if reset_result.returncode != 0:
|
||||
# 如果重置失败,恢复stash(如果有)
|
||||
if has_stashed_changes:
|
||||
subprocess.run(["git", "-C", node_path, "stash", "pop"], check=False)
|
||||
return False, f"Failed to reset branch: {reset_result.stderr}"
|
||||
|
||||
# 操作成功后输出日志
|
||||
if version_offset == 1:
|
||||
print(f"[NodeVersionController] Successfully switched node {node_name} to latest version")
|
||||
else: # 负数偏移量
|
||||
print(f"[NodeVersionController] Successfully went back {abs(version_offset)} versions from latest")
|
||||
|
||||
# 获取切换后的提交信息
|
||||
new_commit_result = subprocess.run(
|
||||
["git", "-C", node_path, "log", "-1", "--pretty=format:%h|%an|%ad|%s", "--date=short"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
if new_commit_result.returncode != 0:
|
||||
return False, "Failed to get new commit information"
|
||||
|
||||
new_commit_info = new_commit_result.stdout.strip().split("|", 3)
|
||||
if len(new_commit_info) >= 4:
|
||||
new_commit_hash = new_commit_info[0]
|
||||
new_commit_message = new_commit_info[3]
|
||||
new_commit_date = new_commit_info[2]
|
||||
else:
|
||||
return False, "Failed to parse new commit information"
|
||||
|
||||
# 根据版本偏移量构建更明确的消息
|
||||
if version_offset == 1:
|
||||
message = f"Successfully switched node {node_name} to latest version: {new_commit_hash}\nCommit message: {new_commit_message}\nCommit date: {new_commit_date}\nBranch: {current_branch}\n"
|
||||
else: # 负数偏移量
|
||||
message = f"Successfully went back {abs(version_offset)} versions to: {new_commit_hash}\nCommit message: {new_commit_message}\nCommit date: {new_commit_date}\nBranch: {current_branch}\n"
|
||||
|
||||
if has_stashed_changes:
|
||||
message += "Note: Uncommitted changes were stashed"
|
||||
|
||||
return True, message
|
||||
except Exception as e:
|
||||
return False, f"Error switching version: {str(e)}"
|
||||
|
||||
# NodeVersionManager class definition - English version
|
||||
class NodeVersionManager:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"node_name": ("STRING", {"default": "", "multiline": False, "placeholder": "Enter node name to switch version"}),
|
||||
"version_offset": ("INT", {"default": 1, "min": -99999, "max": 1, "step": 1, "placeholder": "1=Latest version, -2,-3 etc=Go back from latest"}),
|
||||
"show_history": ("BOOLEAN", {"default": False, "label_on": "Show History", "label_off": "Don't Show History"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("Result Message",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "Update of SD-PPP Plugin"
|
||||
|
||||
def process(self, node_name, version_offset, show_history):
|
||||
controller = NodeVersionController()
|
||||
status_message = ""
|
||||
|
||||
if not node_name.strip():
|
||||
return ("Error: Please enter node name",)
|
||||
|
||||
# 检查节点是否存在
|
||||
node_path = os.path.join(controller.custom_nodes_path, node_name)
|
||||
if not os.path.exists(node_path) or not os.path.isdir(node_path):
|
||||
return (f"Error: Node {node_name} not found",)
|
||||
|
||||
# 如果请求显示历史
|
||||
if show_history:
|
||||
success, result = controller.get_node_commit_history(node_name)
|
||||
if success:
|
||||
status_message += f"Commit history for node {node_name} (branch: {result['branch']}):\n"
|
||||
for i, commit in enumerate(result['commits']):
|
||||
status_message += f"[{i}] {commit['hash'][:7]} | {commit['date']} | {commit['message'][:50]}...\n"
|
||||
else:
|
||||
return (f"Failed to get history: {result}",)
|
||||
else:
|
||||
# 执行版本切换
|
||||
if version_offset == 0:
|
||||
return ("Error: Version offset cannot be 0",)
|
||||
|
||||
success, message = controller.switch_node_version(node_name, version_offset)
|
||||
if success:
|
||||
status_message += message
|
||||
else:
|
||||
return (f"Failed to switch version: {message}",)
|
||||
|
||||
return (status_message,)
|
||||
|
||||
# 节点注册映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"NodeVersionManager": NodeVersionManager
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"NodeVersionManager": "Node Version Switcher"
|
||||
}
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
Binary file not shown.
+846
@@ -0,0 +1,846 @@
|
||||
from server import PromptServer
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
import asyncio
|
||||
import random
|
||||
import re
|
||||
from aiohttp import web
|
||||
import execution
|
||||
import nodes
|
||||
|
||||
CATEGORY_TYPE = "Update of SD-PPP Plugin"
|
||||
|
||||
# ============ 后台执行辅助函数 ============
|
||||
|
||||
def recursive_add_nodes(node_id, old_output, new_output):
|
||||
"""从输出节点递归收集所有依赖节点(与前端 queueManager.recursiveAddNodes 逻辑完全一致)"""
|
||||
current_id = str(node_id)
|
||||
current_node = old_output.get(current_id)
|
||||
|
||||
if not current_node:
|
||||
print(f"[CCXGroupExecutor] 节点 {current_id} 在输出中不存在")
|
||||
return new_output
|
||||
|
||||
if new_output.get(current_id) is None:
|
||||
print(f"[CCXGroupExecutor] 添加节点: {current_id} (类型: {current_node.get('class_type', 'unknown')})")
|
||||
new_output[current_id] = current_node
|
||||
inputs = current_node.get("inputs", {})
|
||||
|
||||
for input_name, input_value in inputs.items():
|
||||
if isinstance(input_value, list):
|
||||
# 标准输入格式: [source_node_id, output_index]
|
||||
source_node_id = input_value[0]
|
||||
if source_node_id is not None and source_node_id != "":
|
||||
print(f"[CCXGroupExecutor] 节点 {current_id} 的输入 {input_name} 来自节点 {source_node_id}")
|
||||
recursive_add_nodes(source_node_id, old_output, new_output)
|
||||
else:
|
||||
print(f"[CCXGroupExecutor] 节点 {current_id} 的输入 {input_name} 没有源节点")
|
||||
elif isinstance(input_value, dict) and "link_id" in input_value:
|
||||
# 某些节点可能使用link_id格式
|
||||
link_id = input_value["link_id"]
|
||||
if link_id:
|
||||
print(f"[CCXGroupExecutor] 节点 {current_id} 的输入 {input_name} 来自link_id {link_id}")
|
||||
recursive_add_nodes(link_id, old_output, new_output)
|
||||
|
||||
return new_output
|
||||
|
||||
def filter_prompt_for_nodes(full_prompt, output_node_ids):
|
||||
"""从完整的 API prompt 中筛选出指定输出节点及其依赖"""
|
||||
filtered_prompt = {}
|
||||
|
||||
for node_id in output_node_ids:
|
||||
# 确保node_id是字符串
|
||||
node_id_str = str(node_id)
|
||||
|
||||
# 首先检查该节点是否存在
|
||||
if node_id_str not in full_prompt:
|
||||
print(f"[CCXGroupExecutor] 警告:输出节点 {node_id_str} 不在完整prompt中")
|
||||
continue
|
||||
|
||||
print(f"[CCXGroupExecutor] 开始筛选节点,输出节点: {node_id_str}")
|
||||
|
||||
# 递归收集所有依赖节点
|
||||
recursive_add_nodes(node_id_str, full_prompt, filtered_prompt)
|
||||
|
||||
print(f"[CCXGroupExecutor] 筛选完成,共收集 {len(filtered_prompt)} 个节点")
|
||||
return filtered_prompt
|
||||
|
||||
class GroupExecutorBackend:
|
||||
"""后台执行管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.running_tasks = {}
|
||||
self.task_lock = threading.Lock()
|
||||
self.interrupted_prompts = set() # 记录被中断的 prompt_id
|
||||
self._setup_interrupt_handler()
|
||||
|
||||
def _setup_interrupt_handler(self):
|
||||
"""设置中断处理器,监听 execution_interrupted 消息"""
|
||||
try:
|
||||
server = PromptServer.instance
|
||||
backend_instance = self
|
||||
|
||||
# 保存原始的 send_sync 方法
|
||||
original_send_sync = server.send_sync
|
||||
|
||||
def patched_send_sync(event, data, sid=None):
|
||||
try:
|
||||
# 调用原始方法,添加错误处理
|
||||
original_send_sync(event, data, sid)
|
||||
|
||||
# 监听 execution_interrupted 事件
|
||||
if event == "execution_interrupted":
|
||||
prompt_id = data.get("prompt_id")
|
||||
if prompt_id:
|
||||
backend_instance.interrupted_prompts.add(prompt_id)
|
||||
# 取消所有后台任务
|
||||
backend_instance._cancel_all_on_interrupt()
|
||||
except Exception as e:
|
||||
# 忽略所有WebSocket和连接相关的错误
|
||||
error_str = str(e)
|
||||
if any(keyword in error_str.lower() for keyword in ["websocket", "socket", "connection", "broken pipe", "clienterror"]):
|
||||
# 完全忽略WebSocket连接错误
|
||||
pass
|
||||
else:
|
||||
print(f"[CCXGroupExecutor] 发送消息失败: {e}")
|
||||
|
||||
server.send_sync = patched_send_sync
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 设置中断监听器失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _cancel_all_on_interrupt(self):
|
||||
"""响应全局中断,取消所有正在运行的后台任务"""
|
||||
with self.task_lock:
|
||||
for node_id, task_info in list(self.running_tasks.items()):
|
||||
if task_info.get("status") == "running" and not task_info.get("cancel"):
|
||||
task_info["cancel"] = True
|
||||
|
||||
def execute_in_background(self, node_id, execution_list, full_api_prompt):
|
||||
"""启动后台执行线程
|
||||
|
||||
Args:
|
||||
node_id: 节点 ID
|
||||
execution_list: 执行列表,每项包含 group_name, repeat_count, delay_seconds, output_node_ids
|
||||
full_api_prompt: 前端生成的完整 API prompt(已经是正确格式)
|
||||
"""
|
||||
with self.task_lock:
|
||||
# 检查是否有真正运行的线程
|
||||
if node_id in self.running_tasks:
|
||||
task = self.running_tasks[node_id]
|
||||
# 检查线程是否还在运行
|
||||
if task.get("thread") and task["thread"].is_alive():
|
||||
print(f"[CCXGroupExecutor] 任务 {node_id} 已经在运行,拒绝重复启动")
|
||||
return False
|
||||
# 如果线程已经结束,清理旧任务状态
|
||||
print(f"[CCXGroupExecutor] 清理任务 {node_id} 的旧状态")
|
||||
del self.running_tasks[node_id]
|
||||
|
||||
# 清理可能存在的中断状态(即使是其他任务的)
|
||||
if hasattr(self, "interrupted_prompts"):
|
||||
print(f"[CCXGroupExecutor] 清理所有旧的中断状态")
|
||||
self.interrupted_prompts.clear()
|
||||
|
||||
thread = threading.Thread(
|
||||
target=self._execute_task,
|
||||
args=(node_id, execution_list, full_api_prompt),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
self.running_tasks[node_id] = {
|
||||
"thread": thread,
|
||||
"status": "running",
|
||||
"cancel": False,
|
||||
"start_time": time.time() # 添加开始时间,便于调试
|
||||
}
|
||||
|
||||
print(f"[CCXGroupExecutor] 成功启动任务 {node_id},线程 ID: {thread.ident}")
|
||||
return True
|
||||
|
||||
def cancel_task(self, node_id):
|
||||
"""取消任务"""
|
||||
with self.task_lock:
|
||||
if node_id in self.running_tasks:
|
||||
self.running_tasks[node_id]["cancel"] = True
|
||||
|
||||
# 中断当前正在执行的任务
|
||||
try:
|
||||
server = PromptServer.instance
|
||||
server.send_sync("interrupt", {})
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 发送中断信号失败: {e}")
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def _execute_task(self, node_id, execution_list, full_api_prompt):
|
||||
"""后台执行任务的核心逻辑
|
||||
|
||||
Args:
|
||||
node_id: 节点 ID
|
||||
execution_list: 执行列表
|
||||
full_api_prompt: 前端生成的完整 API prompt
|
||||
"""
|
||||
print(f"[CCXGroupExecutor] 开始执行任务 node_id={node_id}, 执行列表长度={len(execution_list)}")
|
||||
print(f"[CCXGroupExecutor] 完整执行列表: {execution_list}")
|
||||
|
||||
# 验证执行列表
|
||||
if not isinstance(execution_list, list) or len(execution_list) == 0:
|
||||
print(f"[CCXGroupExecutor] 无效的执行列表: {execution_list}")
|
||||
return
|
||||
|
||||
# 打印执行列表详情,方便调试
|
||||
valid_execution_count = 0
|
||||
for i, exec_item in enumerate(execution_list):
|
||||
group_name = exec_item.get("group_name", "")
|
||||
output_node_ids = exec_item.get("output_node_ids", [])
|
||||
repeat_count = exec_item.get("repeat_count", 1)
|
||||
delay_seconds = exec_item.get("delay_seconds", 0)
|
||||
|
||||
is_valid = True
|
||||
if group_name != "__delay__" and not output_node_ids:
|
||||
is_valid = False
|
||||
|
||||
status = "有效" if is_valid else "无效"
|
||||
print(f"[CCXGroupExecutor] 执行项 {i+1}/{len(execution_list)}: group_name={group_name}, output_node_ids={output_node_ids}, repeat_count={repeat_count}, delay_seconds={delay_seconds} [{status}]")
|
||||
|
||||
if is_valid:
|
||||
valid_execution_count += 1
|
||||
|
||||
if valid_execution_count == 0:
|
||||
print(f"[CCXGroupExecutor] 没有有效的执行项,任务将终止")
|
||||
return
|
||||
|
||||
try:
|
||||
# 确保任务开始时的状态是干净的
|
||||
with self.task_lock:
|
||||
if node_id in self.running_tasks:
|
||||
print(f"[CCXGroupExecutor] 重置任务 {node_id} 的取消标志")
|
||||
self.running_tasks[node_id]["cancel"] = False
|
||||
|
||||
# 清理可能存在的旧中断状态
|
||||
if hasattr(self, "interrupted_prompts"):
|
||||
print(f"[CCXGroupExecutor] 清理旧的中断状态")
|
||||
self.interrupted_prompts.clear()
|
||||
|
||||
# 遍历执行列表中的每个执行项
|
||||
for item_index, exec_item in enumerate(execution_list):
|
||||
# 检查取消标志
|
||||
if self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
print(f"[CCXGroupExecutor] 任务被取消")
|
||||
break
|
||||
|
||||
group_name = exec_item.get("group_name", "")
|
||||
repeat_count = int(exec_item.get("repeat_count", 1))
|
||||
delay_seconds = float(exec_item.get("delay_seconds", 0))
|
||||
output_node_ids = exec_item.get("output_node_ids", [])
|
||||
|
||||
print(f"\n[CCXGroupExecutor] ====== 处理执行项 {item_index+1}/{len(execution_list)} ======")
|
||||
print(f"[CCXGroupExecutor] group_name={group_name}, repeat_count={repeat_count}, delay_seconds={delay_seconds}")
|
||||
print(f"[CCXGroupExecutor] output_node_ids={output_node_ids}")
|
||||
|
||||
# 验证执行项
|
||||
if group_name != "__delay__" and (not group_name or not output_node_ids):
|
||||
print(f"[CCXGroupExecutor] 跳过无效执行项: group_name={group_name}, output_node_ids={output_node_ids}")
|
||||
continue
|
||||
|
||||
# 处理延迟
|
||||
if group_name == "__delay__":
|
||||
print(f"[CCXGroupExecutor] 执行延迟: {delay_seconds}秒")
|
||||
if delay_seconds > 0 and not self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
# 分段延迟,以便能快速响应取消
|
||||
delay_steps = int(delay_seconds * 2) # 每 0.5 秒检查一次
|
||||
for step in range(delay_steps):
|
||||
if self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
print(f"[CCXGroupExecutor] 延迟期间任务被取消")
|
||||
break
|
||||
time.sleep(0.5)
|
||||
if (step + 1) % 2 == 0: # 每1秒打印一次延迟进度
|
||||
print(f"[CCXGroupExecutor] 延迟进度: {int((step + 1) * 0.5)}秒/{delay_seconds}秒")
|
||||
continue
|
||||
|
||||
# 执行 repeat_count 次
|
||||
for repeat_index in range(repeat_count):
|
||||
# 检查取消标志
|
||||
if self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
print(f"[CCXGroupExecutor] 任务被取消")
|
||||
break
|
||||
|
||||
if repeat_count > 1:
|
||||
print(f"[CCXGroupExecutor] 执行组 '{group_name}' ({repeat_index+1}/{repeat_count})")
|
||||
else:
|
||||
print(f"[CCXGroupExecutor] 执行组 '{group_name}'")
|
||||
|
||||
# 从完整 prompt 中筛选出该组需要的节点
|
||||
print(f"[CCXGroupExecutor] 从完整 prompt 中筛选节点,输出节点 ID: {output_node_ids}")
|
||||
prompt = filter_prompt_for_nodes(full_api_prompt, output_node_ids)
|
||||
|
||||
if not prompt:
|
||||
print(f"[CCXGroupExecutor] 筛选 prompt 失败,跳过此执行")
|
||||
continue
|
||||
|
||||
print(f"[CCXGroupExecutor] 筛选出 {len(prompt)} 个节点")
|
||||
|
||||
# 处理随机种子:为每个有 seed 参数的节点生成新的随机值
|
||||
print(f"[CCXGroupExecutor] 处理随机种子")
|
||||
seed_nodes = 0
|
||||
for node_id_str, node_data in prompt.items():
|
||||
if "seed" in node_data.get("inputs", {}):
|
||||
new_seed = random.randint(0, 0xffffffffffffffff)
|
||||
prompt[node_id_str]["inputs"]["seed"] = new_seed
|
||||
seed_nodes += 1
|
||||
# 也处理 noise_seed(某些节点使用这个名称)
|
||||
if "noise_seed" in node_data.get("inputs", {}):
|
||||
new_seed = random.randint(0, 0xffffffffffffffff)
|
||||
prompt[node_id_str]["inputs"]["noise_seed"] = new_seed
|
||||
seed_nodes += 1
|
||||
|
||||
if seed_nodes > 0:
|
||||
print(f"[CCXGroupExecutor] 更新了 {seed_nodes} 个节点的随机种子")
|
||||
|
||||
# 提交到队列
|
||||
print(f"[CCXGroupExecutor] 提交 prompt 到队列")
|
||||
task_info = self._queue_prompt(prompt)
|
||||
|
||||
if task_info:
|
||||
number, prompt_id = task_info
|
||||
print(f"[CCXGroupExecutor] Prompt 提交成功,number={number}, ID: {prompt_id}")
|
||||
# 等待执行完成(返回是否检测到中断)
|
||||
was_interrupted = self._wait_for_completion(task_info, node_id)
|
||||
|
||||
# 如果等待期间检测到中断,立即退出
|
||||
if was_interrupted:
|
||||
print(f"[CCXGroupExecutor] 执行中断")
|
||||
# 使用return而不是break,确保能正确清理资源
|
||||
return
|
||||
else:
|
||||
print(f"[CCXGroupExecutor] 提交 prompt 失败")
|
||||
|
||||
# 延迟(支持中断)
|
||||
if delay_seconds > 0 and repeat_index < repeat_count - 1:
|
||||
print(f"[CCXGroupExecutor] 组执行之间的延迟: {delay_seconds}秒")
|
||||
if not self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
# 分段延迟,以便能快速响应取消
|
||||
delay_steps = int(delay_seconds * 2) # 每 0.5 秒检查一次
|
||||
for step in range(delay_steps):
|
||||
if self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
print(f"[CCXGroupExecutor] 延迟期间任务被取消")
|
||||
break
|
||||
time.sleep(0.5)
|
||||
if (step + 1) % 2 == 0: # 每1秒打印一次延迟进度
|
||||
print(f"[CCXGroupExecutor] 延迟进度: {int((step + 1) * 0.5)}秒/{delay_seconds}秒")
|
||||
|
||||
if self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
print(f"[CCXGroupExecutor] 任务已取消")
|
||||
else:
|
||||
print(f"[CCXGroupExecutor] 所有执行项处理完成,任务执行结束")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 后台执行出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
with self.task_lock:
|
||||
# 任务完成后从 running_tasks 中删除
|
||||
if node_id in self.running_tasks:
|
||||
print(f"[CCXGroupExecutor] 从 running_tasks 中删除任务 {node_id}")
|
||||
del self.running_tasks[node_id]
|
||||
|
||||
# 确保中断状态也被清理
|
||||
if hasattr(self, "interrupted_prompts"):
|
||||
print(f"[CCXGroupExecutor] 清理最终的中断状态")
|
||||
self.interrupted_prompts.clear()
|
||||
|
||||
print(f"[CCXGroupExecutor] 任务 {node_id} 的所有状态已清理")
|
||||
|
||||
def _queue_prompt(self, prompt):
|
||||
"""提交 prompt 到队列"""
|
||||
try:
|
||||
# 基本验证:确保prompt不为空
|
||||
if not prompt or not isinstance(prompt, dict) or len(prompt) == 0:
|
||||
print(f"[CCXGroupExecutor] Prompt 为空或格式无效: {prompt}")
|
||||
return None
|
||||
|
||||
server = PromptServer.instance
|
||||
prompt_id = str(uuid.uuid4())
|
||||
|
||||
print(f"[CCXGroupExecutor] 开始提交 prompt,包含 {len(prompt)} 个节点,prompt_id={prompt_id}")
|
||||
|
||||
# 验证 prompt(validate_prompt 是异步函数,需要在事件循环中运行)
|
||||
try:
|
||||
loop = server.loop
|
||||
# 在事件循环中运行异步函数
|
||||
print(f"[CCXGroupExecutor] 开始验证 prompt,包含节点: {list(prompt.keys())}")
|
||||
valid = asyncio.run_coroutine_threadsafe(
|
||||
execution.validate_prompt(prompt_id, prompt, None),
|
||||
loop
|
||||
).result(timeout=30)
|
||||
except Exception as validate_error:
|
||||
print(f"[CCXGroupExecutor] Prompt 验证出错: {validate_error}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
if not valid[0]:
|
||||
print(f"[CCXGroupExecutor] Prompt 验证失败: {valid[1]}")
|
||||
return None
|
||||
|
||||
# 获取输出节点列表
|
||||
outputs_to_execute = list(valid[2])
|
||||
print(f"[CCXGroupExecutor] Prompt 验证通过,输出节点数量: {len(outputs_to_execute)}")
|
||||
|
||||
# 确保输出节点列表不为空
|
||||
if not outputs_to_execute:
|
||||
print(f"[CCXGroupExecutor] 警告:没有找到输出节点,这可能导致执行失败")
|
||||
|
||||
# 尝试从prompt中找到可能的输出节点
|
||||
possible_outputs = []
|
||||
for node_id, node_data in prompt.items():
|
||||
# 检查是否是已知的输出节点类型
|
||||
class_type = node_data.get('class_type', '')
|
||||
if class_type in ['SaveImage', 'PreviewImage']:
|
||||
possible_outputs.append(node_id)
|
||||
# 也检查是否有'output_node'属性的节点
|
||||
if node_data.get('output_node') == True:
|
||||
possible_outputs.append(node_id)
|
||||
# 检查是否有'outputs'但没有'inputs'的节点
|
||||
if node_data.get('outputs') and not node_data.get('inputs'):
|
||||
possible_outputs.append(node_id)
|
||||
|
||||
if possible_outputs:
|
||||
# 去重
|
||||
outputs_to_execute = list(set(possible_outputs))
|
||||
print(f"[CCXGroupExecutor] 尝试使用可能的输出节点: {outputs_to_execute}")
|
||||
else:
|
||||
# 最后尝试:使用prompt中的最后一个节点作为输出节点
|
||||
if len(prompt) > 0:
|
||||
last_node_id = list(prompt.keys())[-1]
|
||||
outputs_to_execute = [last_node_id]
|
||||
print(f"[CCXGroupExecutor] 没有找到明确的输出节点,使用最后一个节点 {last_node_id} 作为输出")
|
||||
else:
|
||||
print(f"[CCXGroupExecutor] 没有找到任何可能的输出节点,跳过此prompt")
|
||||
return None
|
||||
|
||||
# 构建队列项(确保与ComfyUI的预期格式完全一致)
|
||||
# 格式:(number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
|
||||
extra_data = {}
|
||||
sensitive = {}
|
||||
|
||||
# 验证队列项格式(先使用临时number=0进行验证)
|
||||
temp_queue_item = (0, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
|
||||
print(f"[CCXGroupExecutor] 构建队列项: prompt_id={prompt_id}, 输出节点={outputs_to_execute}")
|
||||
|
||||
# 验证队列项格式
|
||||
if not isinstance(temp_queue_item, tuple) or len(temp_queue_item) != 6:
|
||||
print(f"[CCXGroupExecutor] 队列项格式错误: {temp_queue_item}")
|
||||
return None
|
||||
|
||||
# 只有在所有验证都通过后才递增number
|
||||
number = server.number
|
||||
server.number += 1
|
||||
|
||||
# 使用正确的number构建最终队列项
|
||||
queue_item = (number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
|
||||
print(f"[CCXGroupExecutor] 构建队列项完成: number={number}, prompt_id={prompt_id}, 输出节点={outputs_to_execute}")
|
||||
|
||||
# 提交到队列
|
||||
server.prompt_queue.put(queue_item)
|
||||
|
||||
print(f"[CCXGroupExecutor] Prompt 成功提交到队列,number={number}, prompt_id={prompt_id}")
|
||||
|
||||
# 返回任务编号和prompt_id,用于更准确的状态跟踪
|
||||
return (number, prompt_id)
|
||||
|
||||
except Exception as e:
|
||||
# 修复:在任何异常情况下都回滚server.number的递增
|
||||
if 'server' in locals() and hasattr(server, 'number'):
|
||||
# 只有在已经递增过number的情况下才回滚
|
||||
# 通过检查number是否大于原始值来判断
|
||||
if 'number' in locals() and server.number > number:
|
||||
server.number -= 1
|
||||
print(f"[CCXGroupExecutor] 异常回滚 number 到 {server.number}")
|
||||
print(f"[CCXGroupExecutor] 提交队列失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _wait_for_completion(self, task_info, node_id):
|
||||
"""等待 prompt 执行完成,同时响应取消请求
|
||||
参数: task_info 是包含 (number, prompt_id) 的元组
|
||||
返回: True 如果检测到中断,False 正常完成
|
||||
"""
|
||||
try:
|
||||
server = PromptServer.instance
|
||||
number, prompt_id = task_info
|
||||
|
||||
print(f"[CCXGroupExecutor] 等待任务完成: number={number}, prompt_id={prompt_id}")
|
||||
|
||||
wait_start_time = time.time()
|
||||
max_wait_time = 300 # 最大等待时间5分钟,避免无限等待
|
||||
|
||||
# 连续检查的计数
|
||||
consecutive_checks = 0
|
||||
max_consecutive_checks = 5 # 最多连续检查5次
|
||||
|
||||
while True:
|
||||
# 检查是否超过最大等待时间
|
||||
if time.time() - wait_start_time > max_wait_time:
|
||||
print(f"[CCXGroupExecutor] 任务等待超时 ({max_wait_time}秒): number={number}, prompt_id={prompt_id}")
|
||||
return False # 超时视为正常完成,但实际上可能有问题
|
||||
|
||||
# 检查任务是否已经被移除(可能任务已经完成但我们不知道)
|
||||
if node_id not in self.running_tasks:
|
||||
print(f"[CCXGroupExecutor] 任务节点 {node_id} 已不在运行任务列表中,可能已被清理")
|
||||
return False
|
||||
|
||||
# 检查这个 prompt 是否被中断
|
||||
if prompt_id in self.interrupted_prompts:
|
||||
# 设置任务取消标志
|
||||
with self.task_lock:
|
||||
if node_id in self.running_tasks:
|
||||
self.running_tasks[node_id]["cancel"] = True
|
||||
# 从中断集合中移除
|
||||
self.interrupted_prompts.discard(prompt_id)
|
||||
print(f"[CCXGroupExecutor] 任务被中断: number={number}, prompt_id={prompt_id}")
|
||||
return True # 返回中断状态
|
||||
|
||||
# 检查是否被取消
|
||||
if self.running_tasks.get(node_id, {}).get("cancel"):
|
||||
# 从队列中删除这个 prompt(如果还在队列中)
|
||||
try:
|
||||
def should_delete(item):
|
||||
return len(item) >= 2 and (item[1] == prompt_id or item[0] == number)
|
||||
server.prompt_queue.delete_queue_item(should_delete)
|
||||
except Exception as del_error:
|
||||
print(f"[CCXGroupExecutor] 删除队列项时出错: {del_error}")
|
||||
print(f"[CCXGroupExecutor] 任务被取消: number={number}, prompt_id={prompt_id}")
|
||||
return True # 返回中断状态
|
||||
|
||||
# 检查是否在历史记录中(表示已完成)
|
||||
if prompt_id in server.prompt_queue.history:
|
||||
# 检查是否是因为中断而完成的
|
||||
if prompt_id in self.interrupted_prompts:
|
||||
self.interrupted_prompts.discard(prompt_id)
|
||||
print(f"[CCXGroupExecutor] 任务在历史记录中但被中断: number={number}, prompt_id={prompt_id}")
|
||||
return True
|
||||
print(f"[CCXGroupExecutor] 任务正常完成: number={number}, prompt_id={prompt_id}")
|
||||
return False # 正常完成
|
||||
|
||||
# 检查是否还在队列中
|
||||
running, pending = server.prompt_queue.get_current_queue()
|
||||
|
||||
in_queue = False
|
||||
|
||||
# 检查运行队列
|
||||
for item in running:
|
||||
if len(item) >= 2 and (item[1] == prompt_id or item[0] == number):
|
||||
in_queue = True
|
||||
print(f"[CCXGroupExecutor] 任务仍在运行队列中: number={number}, prompt_id={prompt_id}")
|
||||
consecutive_checks = 0 # 重置检查计数
|
||||
break
|
||||
|
||||
# 检查等待队列
|
||||
if not in_queue:
|
||||
for item in pending:
|
||||
if len(item) >= 2 and (item[1] == prompt_id or item[0] == number):
|
||||
in_queue = True
|
||||
print(f"[CCXGroupExecutor] 任务在等待队列中: number={number}, prompt_id={prompt_id}")
|
||||
consecutive_checks = 0 # 重置检查计数
|
||||
break
|
||||
|
||||
# 如果任务不在队列中且不在历史记录中,增加检查计数
|
||||
if not in_queue and prompt_id not in server.prompt_queue.history:
|
||||
consecutive_checks += 1
|
||||
print(f"[CCXGroupExecutor] 任务不在队列中,已连续检查 {consecutive_checks}/{max_consecutive_checks} 次: number={number}, prompt_id={prompt_id}")
|
||||
|
||||
# 只有连续检查次数达到最大值,才认为任务已经完成
|
||||
if consecutive_checks >= max_consecutive_checks:
|
||||
print(f"[CCXGroupExecutor] 任务连续 {max_consecutive_checks} 次不在队列中且不在历史记录中,认为已完成: number={number}, prompt_id={prompt_id}")
|
||||
return False
|
||||
else:
|
||||
consecutive_checks = 0 # 重置检查计数
|
||||
|
||||
# 正常等待,避免太频繁检查
|
||||
time.sleep(1.0) # 增加等待时间到1秒,减少检查频率
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 等待执行完成时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
# 全局后台执行器实例
|
||||
_backend_executor = GroupExecutorBackend()
|
||||
|
||||
# ============ 节点定义 ============
|
||||
|
||||
import re
|
||||
|
||||
class CCXGroupExecutorSingle:
|
||||
def __init__(self):
|
||||
# 添加执行状态管理
|
||||
self.is_executing = False
|
||||
self.execution_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"group_name": ("STRING", {"multiline": True}),
|
||||
"repeat_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
|
||||
"delay_seconds": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 60.0, "step": 0.1}),
|
||||
},
|
||||
"optional": {
|
||||
"signal": ("SIGNAL",),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SIGNAL",)
|
||||
FUNCTION = "execute_group"
|
||||
CATEGORY = "Update of SD-PPP Plugin"
|
||||
|
||||
def execute_group(self, group_name, repeat_count, delay_seconds, signal=None, unique_id=None):
|
||||
try:
|
||||
# 使用锁确保只有一个执行请求通过
|
||||
with self.execution_lock:
|
||||
if self.is_executing:
|
||||
print(f"[CCXGroupExecutorSingle] 节点已经在执行中,拒绝重复执行请求 (unique_id={unique_id})")
|
||||
# 如果有信号输入,直接返回信号
|
||||
if signal is not None:
|
||||
return (signal,)
|
||||
# 否则返回空信号
|
||||
return (([],),)
|
||||
|
||||
# 设置执行状态
|
||||
self.is_executing = True
|
||||
# 将多行输入拆分为多个组(支持逗号和换行分隔)
|
||||
group_names = [name.strip() for name in re.split(r'[,\n]+', group_name) if name.strip()]
|
||||
execution_list = []
|
||||
|
||||
# 为每个组创建执行项
|
||||
for group in group_names:
|
||||
execution_list.append({
|
||||
"group_name": group,
|
||||
"repeat_count": repeat_count,
|
||||
"delay_seconds": delay_seconds
|
||||
})
|
||||
|
||||
# 如果有信号输入,将信号追加到新执行列表后面(正确的执行顺序:新组先执行,然后执行信号中的组)
|
||||
if signal is not None:
|
||||
if isinstance(signal, list):
|
||||
# 将信号中的执行项追加到新执行列表后面
|
||||
result = execution_list.copy()
|
||||
result.extend(signal)
|
||||
return (result,)
|
||||
else:
|
||||
# 如果信号不是列表,将信号作为单个执行项追加到新执行列表后面
|
||||
result = execution_list.copy()
|
||||
result.append(signal)
|
||||
return (result,)
|
||||
|
||||
# 如果没有信号输入,直接返回执行列表
|
||||
return (execution_list,)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[GroupExecutorMulti {unique_id}] 错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return ({"error": str(e)},)
|
||||
finally:
|
||||
# 无论执行成功还是失败,都重置执行状态
|
||||
with self.execution_lock:
|
||||
self.is_executing = False
|
||||
class CCXGroupExecutorSender:
|
||||
"""执行信号发送节点"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"signal": ("SIGNAL",),
|
||||
"execution_mode": (["前端执行", "后台执行"], {"default": "后台执行"}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = CATEGORY_TYPE
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def execute(self, signal, execution_mode, unique_id=None, prompt=None, extra_pnginfo=None):
|
||||
try:
|
||||
if not signal:
|
||||
raise ValueError("没有收到执行信号")
|
||||
|
||||
execution_list = signal if isinstance(signal, list) else [signal]
|
||||
|
||||
if execution_mode == "后台执行":
|
||||
# 后台执行模式:通知前端生成 API prompt 并发送给后端
|
||||
PromptServer.instance.send_sync(
|
||||
"ccx_execute_group_list_backend", {
|
||||
"node_id": unique_id,
|
||||
"execution_list": execution_list
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
# 前端执行模式(原有方式)
|
||||
PromptServer.instance.send_sync(
|
||||
"ccx_execute_group_list", {
|
||||
"node_id": unique_id,
|
||||
"execution_list": execution_list
|
||||
}
|
||||
)
|
||||
|
||||
return ()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 执行错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return ()
|
||||
|
||||
|
||||
|
||||
|
||||
CONFIG_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "group_configs")
|
||||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||||
|
||||
routes = PromptServer.instance.routes
|
||||
|
||||
@routes.post("/ccx_group_executor/execute_backend")
|
||||
async def execute_backend(request):
|
||||
"""接收前端发送的执行请求,在后台执行组"""
|
||||
try:
|
||||
data = await request.json()
|
||||
node_id = data.get("node_id")
|
||||
execution_list = data.get("execution_list", [])
|
||||
full_api_prompt = data.get("api_prompt", {})
|
||||
|
||||
if not node_id:
|
||||
return web.json_response({"status": "error", "message": "缺少 node_id"}, status=400)
|
||||
|
||||
if not execution_list:
|
||||
return web.json_response({"status": "error", "message": "执行列表为空"}, status=400)
|
||||
|
||||
if not full_api_prompt:
|
||||
return web.json_response({"status": "error", "message": "缺少 API prompt"}, status=400)
|
||||
|
||||
print(f"[CCXGroupExecutor] 收到后台执行请求: node_id={node_id}, 执行项数={len(execution_list)}")
|
||||
|
||||
# 启动后台执行
|
||||
success = _backend_executor.execute_in_background(
|
||||
node_id,
|
||||
execution_list,
|
||||
full_api_prompt
|
||||
)
|
||||
|
||||
if success:
|
||||
return web.json_response({"status": "success", "message": "后台执行已启动"})
|
||||
else:
|
||||
return web.json_response({"status": "error", "message": "已有任务在执行中"}, status=409)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 后台执行请求处理失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@routes.get("/ccx_group_executor/configs")
|
||||
async def get_configs(request):
|
||||
try:
|
||||
|
||||
configs = []
|
||||
for filename in os.listdir(CONFIG_DIR):
|
||||
if filename.endswith('.json'):
|
||||
configs.append({
|
||||
"name": filename[:-5]
|
||||
})
|
||||
return web.json_response({"status": "success", "configs": configs})
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 获取配置失败: {str(e)}")
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@routes.post("/ccx_group_executor/configs")
|
||||
async def save_config(request):
|
||||
try:
|
||||
print("[CCXGroupExecutor] 收到保存配置请求")
|
||||
data = await request.json()
|
||||
config_name = data.get('name')
|
||||
if not config_name:
|
||||
return web.json_response({"status": "error", "message": "配置名称不能为空"}, status=400)
|
||||
|
||||
safe_name = "".join(c for c in config_name if c.isalnum() or c in (' ', '-', '_'))
|
||||
filename = os.path.join(CONFIG_DIR, f"{safe_name}.json")
|
||||
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[CCXGroupExecutor] 配置已保存: {filename}")
|
||||
return web.json_response({"status": "success"})
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[CCXGroupExecutor] JSON解析错误: {str(e)}")
|
||||
return web.json_response({"status": "error", "message": f"JSON格式错误: {str(e)}"}, status=400)
|
||||
except Exception as e:
|
||||
print(f"[CCXGroupExecutor] 保存配置失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@routes.get('/ccx_group_executor/configs/{name}')
|
||||
async def get_config(request):
|
||||
try:
|
||||
config_name = request.match_info.get('name')
|
||||
if not config_name:
|
||||
return web.json_response({"error": "配置名称不能为空"}, status=400)
|
||||
|
||||
filename = os.path.join(CONFIG_DIR, f"{config_name}.json")
|
||||
if not os.path.exists(filename):
|
||||
return web.json_response({"error": "配置不存在"}, status=404)
|
||||
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
return web.json_response(config)
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.delete('/ccx_group_executor/configs/{name}')
|
||||
async def delete_config(request):
|
||||
try:
|
||||
config_name = request.match_info.get('name')
|
||||
if not config_name:
|
||||
return web.json_response({"error": "配置名称不能为空"}, status=400)
|
||||
|
||||
filename = os.path.join(CONFIG_DIR, f"{config_name}.json")
|
||||
if not os.path.exists(filename):
|
||||
return web.json_response({"error": "配置不存在"}, status=404)
|
||||
|
||||
os.remove(filename)
|
||||
return web.json_response({"status": "success"})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
# 导出节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CCXGroupExecutorSingle": CCXGroupExecutorSingle,
|
||||
"CCXGroupExecutorSender": CCXGroupExecutorSender
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CCXGroupExecutorSingle": "🎈CCX Group Executor (Single)",
|
||||
"CCXGroupExecutorSender": "🎈CCX Group Executor (Sender)"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# This file has been cleaned up.
|
||||
# All redundant nodes have been removed.
|
||||
# Only the "Execution Order Group" and "Operation sequence group" nodes are retained in lgutils.py.
|
||||
@@ -0,0 +1,19 @@
|
||||
[project]
|
||||
name = "comfyui-ccxmanager"
|
||||
description = "Set it to run once, and if there are updates in SD-PPP (Photoshop side and Comfyui side), it will automatically synchronize the updates for you, completely eliminating the need to manually update plugins and nodes."
|
||||
|
||||
version = "1.0.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["requests>=2.25.0"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/WWWEN8/ComfyUI-CCXManager"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "WWWEN8"
|
||||
DisplayName = "comfyui-ccxmanager"
|
||||
Icon = ""
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
requests>=2.25.0
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"repos": [
|
||||
{
|
||||
"repo_url": "https://github.com/zombieyang/sd-ppp.git",
|
||||
"node_name": "sd-ppp",
|
||||
"branch": "main",
|
||||
"auto_update": true,
|
||||
"last_commit_sha": "4c4a3203c97490a404b4a3fc7e00d526fd61e0bd",
|
||||
"last_update_time": ""
|
||||
}
|
||||
],
|
||||
"auto_update_on_start": true,
|
||||
"last_check_time": "2025-10-20 11:39:35",
|
||||
"check_interval_days": 1,
|
||||
"max_workers": 5
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { queueManager } from "./queue_utils.js";
|
||||
class BaseNode extends LGraphNode {
|
||||
static defaultComfyClass = "BaseNode";
|
||||
constructor(title, comfyClass) {
|
||||
super(title);
|
||||
this.isVirtualNode = false;
|
||||
this.configuring = false;
|
||||
this.__constructed__ = false;
|
||||
this.widgets = this.widgets || [];
|
||||
this.properties = this.properties || {};
|
||||
this.comfyClass = comfyClass || this.constructor.comfyClass || BaseNode.defaultComfyClass;
|
||||
setTimeout(() => {
|
||||
this.checkAndRunOnConstructed();
|
||||
});
|
||||
}
|
||||
checkAndRunOnConstructed() {
|
||||
if (!this.__constructed__) {
|
||||
this.onConstructed();
|
||||
}
|
||||
return this.__constructed__;
|
||||
}
|
||||
onConstructed() {
|
||||
if (this.__constructed__) return false;
|
||||
this.type = this.type ?? undefined;
|
||||
this.__constructed__ = true;
|
||||
return this.__constructed__;
|
||||
}
|
||||
configure(info) {
|
||||
this.configuring = true;
|
||||
super.configure(info);
|
||||
for (const w of this.widgets || []) {
|
||||
w.last_y = w.last_y || 0;
|
||||
}
|
||||
this.configuring = false;
|
||||
}
|
||||
static setUp() {
|
||||
if (!this.type) {
|
||||
throw new Error(`Missing type for ${this.name}: ${this.title}`);
|
||||
}
|
||||
LiteGraph.registerNodeType(this.type, this);
|
||||
if (this._category) {
|
||||
this.category = this._category;
|
||||
}
|
||||
}
|
||||
}
|
||||
class CCXGroupExecutorNode extends BaseNode {
|
||||
static type = "🎈CCXGroupExecutor";
|
||||
static title = "🎈CCX Group Executor";
|
||||
static category = "🎈LAOGOU/Group";
|
||||
static _category = "🎈LAOGOU/Group";
|
||||
constructor(title = CCXGroupExecutorNode.title) {
|
||||
super(title, null);
|
||||
this.isVirtualNode = true;
|
||||
this.addProperty("groupCount", 1, "int");
|
||||
this.addProperty("groups", [], "array");
|
||||
this.addProperty("isExecuting", false, "boolean");
|
||||
this.addProperty("repeatCount", 1, "int");
|
||||
this.addProperty("delaySeconds", 0, "number");
|
||||
const groupCountWidget = ComfyWidgets["INT"](this, "groupCount", ["INT", {
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 1,
|
||||
default: 1
|
||||
}], app);
|
||||
const repeatCountWidget = ComfyWidgets["INT"](this, "repeatCount", ["INT", {
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
default: 1,
|
||||
label: "Repeat Count",
|
||||
tooltip: "执行重复次数"
|
||||
}], app);
|
||||
const delayWidget = ComfyWidgets["FLOAT"](this, "delaySeconds", ["FLOAT", {
|
||||
min: 0,
|
||||
max: 300,
|
||||
step: 0.1,
|
||||
default: 0,
|
||||
label: "Delay (s)",
|
||||
tooltip: "队列之间的延迟时间(秒)"
|
||||
}], app);
|
||||
if (repeatCountWidget.widget && delayWidget.widget) {
|
||||
const widgets = [repeatCountWidget.widget, delayWidget.widget];
|
||||
widgets.forEach((widget, index) => {
|
||||
const widgetIndex = this.widgets.indexOf(widget);
|
||||
if (widgetIndex !== -1) {
|
||||
const w = this.widgets.splice(widgetIndex, 1)[0];
|
||||
this.widgets.splice(1 + index, 0, w);
|
||||
}
|
||||
});
|
||||
}
|
||||
groupCountWidget.widget.callback = (v) => {
|
||||
this.properties.groupCount = Math.max(1, Math.min(10, parseInt(v) || 1));
|
||||
this.updateGroupWidgets();
|
||||
};
|
||||
repeatCountWidget.widget.callback = (v) => {
|
||||
this.properties.repeatCount = Math.max(1, Math.min(100, parseInt(v) || 1));
|
||||
};
|
||||
delayWidget.widget.callback = (v) => {
|
||||
this.properties.delaySeconds = Math.max(0, Math.min(300, parseFloat(v) || 0));
|
||||
};
|
||||
this.addWidget("button", "Execute Groups", "Execute", () => {
|
||||
this.executeGroups();
|
||||
});
|
||||
this.addWidget("button", "Cancel", "Cancel", () => {
|
||||
this.cancelExecution();
|
||||
});
|
||||
this.addProperty("isCancelling", false, "boolean");
|
||||
this.updateGroupWidgets();
|
||||
const self = this;
|
||||
app.canvas.onDrawBackground = (() => {
|
||||
const original = app.canvas.onDrawBackground;
|
||||
return function() {
|
||||
self.updateGroupList();
|
||||
return original?.apply(this, arguments);
|
||||
};
|
||||
})();
|
||||
this.originalTitle = title;
|
||||
}
|
||||
getGroupNames() {
|
||||
return [...app.graph._groups].map(g => g.title).sort();
|
||||
}
|
||||
getGroupOutputNodes(groupName) {
|
||||
const group = app.graph._groups.find(g => g.title === groupName);
|
||||
if (!group) {
|
||||
console.warn(`[GroupExecutor] 未找到名为 "${groupName}" 的组`);
|
||||
return [];
|
||||
}
|
||||
const groupNodes = [];
|
||||
for (const node of app.graph._nodes) {
|
||||
if (!node || !node.pos) continue;
|
||||
if (LiteGraph.overlapBounding(group._bounding, node.getBounding())) {
|
||||
groupNodes.push(node);
|
||||
}
|
||||
}
|
||||
group._nodes = groupNodes;
|
||||
return this.getOutputNodes(group._nodes);
|
||||
}
|
||||
getOutputNodes(nodes) {
|
||||
return nodes.filter((n) => {
|
||||
return n.mode !== LiteGraph.NEVER &&
|
||||
n.constructor.nodeData?.output_node === true;
|
||||
});
|
||||
}
|
||||
updateGroupWidgets() {
|
||||
const currentGroups = [...this.properties.groups];
|
||||
this.properties.groups = new Array(this.properties.groupCount).fill("").map((_, i) =>
|
||||
currentGroups[i] || ""
|
||||
);
|
||||
this.widgets = this.widgets.filter(w =>
|
||||
w.name === "groupCount" ||
|
||||
w.name === "repeatCount" ||
|
||||
w.name === "delaySeconds" ||
|
||||
w.name === "Execute Groups" ||
|
||||
w.name === "Cancel"
|
||||
);
|
||||
const executeButton = this.widgets.find(w => w.name === "Execute Groups");
|
||||
const cancelButton = this.widgets.find(w => w.name === "Cancel");
|
||||
if (executeButton) {
|
||||
this.widgets = this.widgets.filter(w => w.name !== "Execute Groups");
|
||||
}
|
||||
if (cancelButton) {
|
||||
this.widgets = this.widgets.filter(w => w.name !== "Cancel");
|
||||
}
|
||||
const groupNames = this.getGroupNames();
|
||||
for (let i = 0; i < this.properties.groupCount; i++) {
|
||||
const widget = this.addWidget(
|
||||
"combo",
|
||||
`Group #${i + 1}`,
|
||||
this.properties.groups[i] || "",
|
||||
(v) => {
|
||||
this.properties.groups[i] = v;
|
||||
},
|
||||
{
|
||||
values: groupNames
|
||||
}
|
||||
);
|
||||
}
|
||||
if (executeButton) {
|
||||
this.widgets.push(executeButton);
|
||||
}
|
||||
if (cancelButton) {
|
||||
this.widgets.push(cancelButton);
|
||||
}
|
||||
this.size = this.computeSize();
|
||||
}
|
||||
updateGroupList() {
|
||||
const groups = this.getGroupNames();
|
||||
this.widgets.forEach(w => {
|
||||
if (w.type === "combo") {
|
||||
w.options.values = groups;
|
||||
}
|
||||
});
|
||||
}
|
||||
async delay(seconds) {
|
||||
if (seconds <= 0) return;
|
||||
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
||||
}
|
||||
updateStatus(text) {
|
||||
this.title = `${this.originalTitle} - ${text}`;
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
resetStatus() {
|
||||
this.title = this.originalTitle;
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
async cancelExecution() {
|
||||
if (!this.properties.isExecuting) {
|
||||
console.warn('[GroupExecutor] 没有正在执行的任务');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.properties.isCancelling = true;
|
||||
this.updateStatus("已取消");
|
||||
await api.interrupt();
|
||||
setTimeout(() => this.resetStatus(), 2000);
|
||||
} catch (error) {
|
||||
console.error('[GroupExecutor] 取消执行时出错:', error);
|
||||
this.updateStatus(`取消失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
async executeGroups() {
|
||||
if (this.properties.isExecuting) {
|
||||
console.warn('[GroupExecutor] 已有执行任务在进行中');
|
||||
return;
|
||||
}
|
||||
this.properties.isExecuting = true;
|
||||
this.properties.isCancelling = false;
|
||||
const totalSteps = this.properties.repeatCount * this.properties.groupCount;
|
||||
let currentStep = 0;
|
||||
try {
|
||||
for (let repeat = 0; repeat < this.properties.repeatCount; repeat++) {
|
||||
for (let i = 0; i < this.properties.groupCount; i++) {
|
||||
if (this.properties.isCancelling) {
|
||||
console.log('[GroupExecutor] 执行被用户取消');
|
||||
await api.interrupt();
|
||||
this.updateStatus("已取消");
|
||||
setTimeout(() => this.resetStatus(), 2000);
|
||||
return;
|
||||
}
|
||||
const groupName = this.properties.groups[i];
|
||||
if (!groupName) continue;
|
||||
currentStep++;
|
||||
this.updateStatus(
|
||||
`${currentStep}/${totalSteps} - ${groupName}`
|
||||
);
|
||||
const outputNodes = this.getGroupOutputNodes(groupName);
|
||||
if (outputNodes && outputNodes.length > 0) {
|
||||
try {
|
||||
const nodeIds = outputNodes.map(n => n.id);
|
||||
try {
|
||||
if (this.properties.isCancelling) {
|
||||
return;
|
||||
}
|
||||
await queueManager.queueOutputNodes(nodeIds);
|
||||
await this.waitForQueue();
|
||||
} catch (queueError) {
|
||||
if (this.properties.isCancelling) {
|
||||
return;
|
||||
}
|
||||
console.warn(`[GroupExecutorSender] 队列执行失败,使用默认方式:`, queueError);
|
||||
for (const n of outputNodes) {
|
||||
if (this.properties.isCancelling) {
|
||||
return;
|
||||
}
|
||||
if (n.triggerQueue) {
|
||||
await n.triggerQueue();
|
||||
await this.waitForQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i < this.properties.groupCount - 1) {
|
||||
if (this.properties.isCancelling) {
|
||||
return;
|
||||
}
|
||||
this.updateStatus(
|
||||
`等待 ${this.properties.delaySeconds}s...`
|
||||
);
|
||||
await this.delay(this.properties.delaySeconds);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[GroupExecutor] 执行组 ${groupName} 时发生错误:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (repeat < this.properties.repeatCount - 1) {
|
||||
if (this.properties.isCancelling) {
|
||||
return;
|
||||
}
|
||||
await this.delay(this.properties.delaySeconds);
|
||||
}
|
||||
}
|
||||
if (!this.properties.isCancelling) {
|
||||
this.updateStatus("完成");
|
||||
setTimeout(() => this.resetStatus(), 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[GroupExecutor] 执行错误:', error);
|
||||
this.updateStatus(`错误: ${error.message}`);
|
||||
app.ui.dialog.show(`执行错误: ${error.message}`);
|
||||
} finally {
|
||||
this.properties.isExecuting = false;
|
||||
this.properties.isCancelling = false;
|
||||
}
|
||||
}
|
||||
async getQueueStatus() {
|
||||
try {
|
||||
const response = await fetch('/queue');
|
||||
const data = await response.json();
|
||||
return {
|
||||
isRunning: data.queue_running.length > 0,
|
||||
isPending: data.queue_pending.length > 0,
|
||||
runningCount: data.queue_running.length,
|
||||
pendingCount: data.queue_pending.length,
|
||||
rawRunning: data.queue_running,
|
||||
rawPending: data.queue_pending
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[GroupExecutor] 获取队列状态失败:', error);
|
||||
// 修复:在获取队列状态失败时,返回队列正在运行的状态
|
||||
// 这样可以避免waitForQueue方法立即返回,从而避免重复执行
|
||||
return {
|
||||
isRunning: true,
|
||||
isPending: false,
|
||||
runningCount: 1,
|
||||
pendingCount: 0,
|
||||
rawRunning: [],
|
||||
rawPending: []
|
||||
};
|
||||
}
|
||||
}
|
||||
async waitForQueue() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkQueue = async () => {
|
||||
try {
|
||||
const status = await this.getQueueStatus();
|
||||
// 确保队列完全空闲:没有正在运行的任务,也没有待处理的任务
|
||||
if (!status.isRunning && !status.isPending) {
|
||||
// 添加一个小延迟,确保队列状态完全更新
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
// 更频繁地检查队列状态,确保及时响应
|
||||
setTimeout(checkQueue, 300);
|
||||
} catch (error) {
|
||||
console.warn(`[GroupExecutor] 检查队列状态失败:`, error);
|
||||
// 即使检查失败,也继续尝试,避免卡住
|
||||
setTimeout(checkQueue, 300);
|
||||
}
|
||||
};
|
||||
checkQueue();
|
||||
});
|
||||
}
|
||||
computeSize() {
|
||||
const widgetHeight = 28;
|
||||
const padding = 4;
|
||||
const width = 200;
|
||||
const height = (this.properties.groupCount + 4) * widgetHeight + padding * 2;
|
||||
return [width, height];
|
||||
}
|
||||
static setUp() {
|
||||
LiteGraph.registerNodeType(this.type, this);
|
||||
this.category = this._category;
|
||||
}
|
||||
serialize() {
|
||||
const data = super.serialize();
|
||||
data.properties = {
|
||||
...data.properties,
|
||||
groupCount: parseInt(this.properties.groupCount) || 1,
|
||||
groups: [...this.properties.groups],
|
||||
// 不持久化执行状态,避免重启后状态异常
|
||||
// isExecuting: this.properties.isExecuting,
|
||||
repeatCount: parseInt(this.properties.repeatCount) || 1,
|
||||
delaySeconds: parseFloat(this.properties.delaySeconds) || 0
|
||||
};
|
||||
return data;
|
||||
}
|
||||
configure(info) {
|
||||
super.configure(info);
|
||||
if (info.properties) {
|
||||
this.properties.groupCount = parseInt(info.properties.groupCount) || 1;
|
||||
this.properties.groups = info.properties.groups ? [...info.properties.groups] : [];
|
||||
// 强制重置执行状态,避免加载时状态异常
|
||||
this.properties.isExecuting = false;
|
||||
this.properties.isCancelling = false;
|
||||
this.properties.repeatCount = parseInt(info.properties.repeatCount) || 1;
|
||||
this.properties.delaySeconds = parseFloat(info.properties.delaySeconds) || 0;
|
||||
}
|
||||
this.widgets.forEach(w => {
|
||||
if (w.name === "groupCount") {
|
||||
w.value = this.properties.groupCount;
|
||||
} else if (w.name === "repeatCount") {
|
||||
w.value = this.properties.repeatCount;
|
||||
} else if (w.name === "delaySeconds") {
|
||||
w.value = this.properties.delaySeconds;
|
||||
}
|
||||
});
|
||||
if (!this.configuring) {
|
||||
this.updateGroupWidgets();
|
||||
}
|
||||
}
|
||||
}
|
||||
app.registerExtension({
|
||||
name: "CCXGroupExecutor",
|
||||
registerCustomNodes() {
|
||||
CCXGroupExecutorNode.setUp();
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "GroupExecutorQueueManager",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (api.fetchApi._isGroupExecutorQueueManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFetchApi = api.fetchApi;
|
||||
|
||||
function collectRelatedNodes(prompt, nodeId, relevantNodes) {
|
||||
if (!prompt[nodeId] || relevantNodes.has(nodeId)) return;
|
||||
relevantNodes.add(nodeId);
|
||||
|
||||
const node = prompt[nodeId];
|
||||
if (node.inputs) {
|
||||
Object.values(node.inputs).forEach(input => {
|
||||
if (input && input.length > 0) {
|
||||
collectRelatedNodes(prompt, input[0], relevantNodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newFetchApi = async function(url, options = {}) {
|
||||
|
||||
if (url === '/prompt' && options.method === 'POST') {
|
||||
const requestData = JSON.parse(options.body);
|
||||
|
||||
if (requestData.extra_data?.isGroupExecutorRequest) {
|
||||
return originalFetchApi.call(api, url, options);
|
||||
}
|
||||
|
||||
const prompt = requestData.prompt;
|
||||
|
||||
// 修改:检查 CCXGroupExecutorSender 而不是旧的 GroupExecutorSender
|
||||
const hasGroupExecutor = Object.values(prompt).some(node =>
|
||||
node.class_type === "CCXGroupExecutorSender"
|
||||
);
|
||||
|
||||
if (hasGroupExecutor) {
|
||||
|
||||
const relevantNodes = new Set();
|
||||
|
||||
for (const [nodeId, node] of Object.entries(prompt)) {
|
||||
// 修改:查找 CCXGroupExecutorSender 节点
|
||||
if (node.class_type === "CCXGroupExecutorSender") {
|
||||
collectRelatedNodes(prompt, nodeId, relevantNodes);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredPrompt = {};
|
||||
for (const nodeId of relevantNodes) {
|
||||
if (prompt[nodeId]) {
|
||||
filteredPrompt[nodeId] = prompt[nodeId];
|
||||
}
|
||||
}
|
||||
|
||||
const modifiedOptions = {
|
||||
...options,
|
||||
body: JSON.stringify({
|
||||
...requestData,
|
||||
prompt: filteredPrompt,
|
||||
extra_data: {
|
||||
...requestData.extra_data,
|
||||
isGroupExecutorRequest: true
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return originalFetchApi.call(api, url, modifiedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
return originalFetchApi.call(api, url, options);
|
||||
};
|
||||
|
||||
newFetchApi._isGroupExecutorQueueManager = true;
|
||||
|
||||
api.fetchApi = newFetchApi;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,679 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { queueManager, getOutputNodes } from "./queue_utils.js";
|
||||
|
||||
// 全局标志,用于确保事件监听器只被注册一次
|
||||
let eventListenersRegistered = false;
|
||||
|
||||
// 全局执行锁,确保同一时间只有一个执行请求在处理
|
||||
let globalExecutionLock = false;
|
||||
|
||||
app.registerExtension({
|
||||
name: "CCXGroupExecutorSender",
|
||||
async setup() {
|
||||
// 修复:在图加载完成后重置所有GroupExecutorSender节点的状态
|
||||
app.addEventListener("graphLoaded", () => {
|
||||
console.log("[CCXGroupExecutorSender] 图加载完成,重置所有发送者节点状态...");
|
||||
const senderNodes = app.graph._nodes.filter(n => n.type === "CCXGroupExecutorSender");
|
||||
senderNodes.forEach(node => {
|
||||
if (node.resetExecutionStatus) {
|
||||
node.resetExecutionStatus();
|
||||
} else {
|
||||
// 如果没有resetExecutionStatus方法,直接重置属性
|
||||
node.properties.isExecuting = false;
|
||||
node.properties.isCancelling = false;
|
||||
node.properties.statusText = "";
|
||||
node.properties.showStatus = false;
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "CCXGroupExecutorSender") {
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
this.properties = {
|
||||
...this.properties,
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
statusText: "",
|
||||
showStatus: false
|
||||
};
|
||||
|
||||
this.size = this.computeSize();
|
||||
};
|
||||
|
||||
// 在节点注册时重置所有执行状态
|
||||
nodeType.prototype.onAddedToGraph = function() {
|
||||
this.properties = {
|
||||
...this.properties,
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
statusText: "",
|
||||
showStatus: false
|
||||
};
|
||||
};
|
||||
|
||||
// 修复:确保执行状态不会被持久化到json文件中
|
||||
const originalSerialize = nodeType.prototype.serialize;
|
||||
nodeType.prototype.serialize = function() {
|
||||
const data = originalSerialize?.apply(this, arguments) || {};
|
||||
if (data.properties) {
|
||||
// 移除执行状态相关的属性,避免持久化
|
||||
delete data.properties.isExecuting;
|
||||
delete data.properties.isCancelling;
|
||||
delete data.properties.statusText;
|
||||
delete data.properties.showStatus;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// 修复:在节点配置时确保状态正确初始化
|
||||
const originalConfigure = nodeType.prototype.configure;
|
||||
nodeType.prototype.configure = function(info) {
|
||||
if (originalConfigure) {
|
||||
originalConfigure.apply(this, arguments);
|
||||
}
|
||||
// 强制重置执行状态
|
||||
this.properties.isExecuting = false;
|
||||
this.properties.isCancelling = false;
|
||||
this.properties.statusText = "";
|
||||
this.properties.showStatus = false;
|
||||
};
|
||||
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
nodeType.prototype.onDrawForeground = function(ctx) {
|
||||
const r = onDrawForeground?.apply?.(this, arguments);
|
||||
|
||||
if (!this.flags.collapsed && this.properties.showStatus) {
|
||||
const text = this.properties.statusText;
|
||||
if (text) {
|
||||
ctx.save();
|
||||
|
||||
ctx.font = "bold 30px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
ctx.fillStyle = this.properties.isExecuting ? "dodgerblue" : "limegreen";
|
||||
|
||||
const centerX = this.size[0] / 2;
|
||||
const centerY = this.size[1] / 2 + 10;
|
||||
|
||||
ctx.fillText(text, centerX, centerY);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
nodeType.prototype.computeSize = function() {
|
||||
return [400, 100]; // 固定宽度和高度
|
||||
};
|
||||
|
||||
nodeType.prototype.updateStatus = function(text) {
|
||||
this.properties.statusText = text;
|
||||
this.properties.showStatus = true;
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
nodeType.prototype.resetStatus = function() {
|
||||
this.properties.statusText = "";
|
||||
this.properties.showStatus = false;
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
nodeType.prototype.getGroupOutputNodes = function(groupName) {
|
||||
console.log(`[CCXGroupExecutorSender] 获取组 "${groupName}" 的输出节点`);
|
||||
|
||||
// 首先尝试通过标题查找组
|
||||
let group = app.graph._groups.find(g => g.title === groupName);
|
||||
|
||||
// 如果找不到,尝试通过ID查找组(可能组的标题有空格或特殊字符)
|
||||
if (!group) {
|
||||
group = app.graph._groups.find(g => g.id === groupName);
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
console.warn(`[CCXGroupExecutorSender] 未找到名为 "${groupName}" 的组`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[CCXGroupExecutorSender] 找到组: ID=${group.id}, 标题="${group.title}", 边界=${JSON.stringify(group._bounding)}`);
|
||||
|
||||
// 确保组的边界和内部节点是最新的
|
||||
if (group.recomputeInsideNodes) {
|
||||
console.log(`[CCXGroupExecutorSender] 调用 recomputeInsideNodes 更新组 ${group.id} 的内部节点`);
|
||||
group.recomputeInsideNodes();
|
||||
}
|
||||
|
||||
// 强制重新计算组内节点,确保始终使用最新的节点列表
|
||||
let groupNodes = [];
|
||||
for (const node of app.graph._nodes) {
|
||||
if (!node || !node.pos) continue;
|
||||
|
||||
const nodeBound = node.getBounding();
|
||||
if (LiteGraph.overlapBounding(group._bounding, nodeBound)) {
|
||||
groupNodes.push(node);
|
||||
console.log(`[CCXGroupExecutorSender] 节点 ${node.id} (${node.type}) 位于组内,位置: ${JSON.stringify(node.pos)}`);
|
||||
}
|
||||
}
|
||||
|
||||
group._nodes = groupNodes;
|
||||
console.log(`[CCXGroupExecutorSender] 组 "${groupName}" 包含 ${groupNodes.length} 个节点`);
|
||||
|
||||
// 筛选出输出节点
|
||||
const outputNodes = this.getOutputNodes(groupNodes);
|
||||
|
||||
// 额外验证:确保输出节点真的在组内
|
||||
const validOutputNodes = outputNodes.filter(node => {
|
||||
const nodeBound = node.getBounding();
|
||||
const isInside = LiteGraph.overlapBounding(group._bounding, nodeBound);
|
||||
if (!isInside) {
|
||||
console.warn(`[CCXGroupExecutorSender] 输出节点 ${node.id} 标记为组内节点,但实际上不在组边界内`);
|
||||
}
|
||||
return isInside;
|
||||
});
|
||||
|
||||
console.log(`[CCXGroupExecutorSender] 组 "${groupName}" 找到 ${validOutputNodes.length} 个有效输出节点:`, validOutputNodes.map(n => ({id: n.id, type: n.type})));
|
||||
|
||||
return validOutputNodes;
|
||||
};
|
||||
|
||||
nodeType.prototype.getOutputNodes = function(nodes) {
|
||||
return nodes.filter((n) => {
|
||||
return n.mode !== LiteGraph.NEVER &&
|
||||
n.constructor.nodeData?.output_node === true;
|
||||
});
|
||||
};
|
||||
|
||||
// 后台执行:生成 API prompt 并发送给后端
|
||||
nodeType.prototype.executeInBackend = async function(executionList) {
|
||||
try {
|
||||
// 修复:检查节点是否已经在执行中,如果是则直接返回,避免重复执行
|
||||
if (this.properties.isExecuting) {
|
||||
console.warn('[CCXGroupExecutorSender] 节点已经在执行中,拒绝重复执行请求');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置执行状态
|
||||
this.properties.isExecuting = true;
|
||||
|
||||
console.log(`[CCXGroupExecutorSender] 开始后台执行,执行列表长度: ${executionList.length}`);
|
||||
console.log(`[CCXGroupExecutorSender] 原始执行列表:`, executionList);
|
||||
|
||||
// 1. 为每个执行项收集输出节点 ID(在生成 prompt 之前,确保节点信息准确)
|
||||
const enrichedExecutionList = [];
|
||||
let hasValidExecutionItem = false;
|
||||
|
||||
// 保存原始的节点模式,确保在获取输出节点时所有节点都能被检测到
|
||||
const originalModes = {};
|
||||
const allNodes = app.graph._nodes;
|
||||
|
||||
// 临时将所有节点模式设置为ALWAYS,确保输出节点检测准确
|
||||
allNodes.forEach(node => {
|
||||
originalModes[node.id] = node.mode;
|
||||
node.mode = LiteGraph.ALWAYS;
|
||||
});
|
||||
|
||||
try {
|
||||
// 遍历原始执行列表,保持顺序
|
||||
for (let i = 0; i < executionList.length; i++) {
|
||||
const exec = executionList[i];
|
||||
const groupName = exec.group_name || '';
|
||||
|
||||
console.log(`[CCXGroupExecutorSender] 处理执行项 ${i + 1}/${executionList.length}: group_name=${groupName}`);
|
||||
|
||||
// 延迟项直接添加
|
||||
if (groupName === "__delay__") {
|
||||
console.log(`[CCXGroupExecutorSender] 添加延迟项: ${exec.delay_seconds}秒`);
|
||||
enrichedExecutionList.push(exec);
|
||||
hasValidExecutionItem = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!groupName) {
|
||||
console.warn(`[CCXGroupExecutorSender] 跳过空的组名称`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取组内的输出节点
|
||||
const outputNodes = this.getGroupOutputNodes(groupName);
|
||||
if (!outputNodes || outputNodes.length === 0) {
|
||||
console.warn(`[CCXGroupExecutorSender] 组 "${groupName}" 中没有输出节点,跳过该组`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const outputNodeIds = outputNodes.map(n => n.id);
|
||||
console.log(`[CCXGroupExecutorSender] 组 "${groupName}" 有 ${outputNodeIds.length} 个输出节点: ${outputNodeIds}`);
|
||||
|
||||
const enrichedExec = {
|
||||
...exec,
|
||||
output_node_ids: outputNodeIds
|
||||
};
|
||||
|
||||
enrichedExecutionList.push(enrichedExec);
|
||||
hasValidExecutionItem = true;
|
||||
console.log(`[CCXGroupExecutorSender] 已添加执行项到丰富列表:`, enrichedExec);
|
||||
}
|
||||
} finally {
|
||||
// 恢复原始节点模式
|
||||
allNodes.forEach(node => {
|
||||
if (originalModes[node.id] !== undefined) {
|
||||
node.mode = originalModes[node.id];
|
||||
}
|
||||
});
|
||||
console.log(`[CCXGroupExecutorSender] 已恢复所有节点的原始模式(输出节点检测阶段)`);
|
||||
}
|
||||
|
||||
if (!hasValidExecutionItem) {
|
||||
throw new Error("没有有效的执行项,请检查组名称和输出节点设置");
|
||||
}
|
||||
|
||||
console.log(`[CCXGroupExecutorSender] 完成执行列表丰富化,有效执行项数量: ${enrichedExecutionList.length}`);
|
||||
console.log(`[CCXGroupExecutorSender] 丰富后的执行列表:`, enrichedExecutionList);
|
||||
|
||||
// 2. 生成完整的 API prompt(在收集完所有节点信息后)
|
||||
console.log(`[CCXGroupExecutorSender] 生成完整的 API prompt...`);
|
||||
|
||||
// 保存原始的节点模式,确保所有节点都被包含在API prompt中
|
||||
const originalModesApi = {};
|
||||
const allNodesApi = app.graph._nodes;
|
||||
|
||||
// 临时将所有节点模式设置为ALWAYS,确保它们都被包含在API prompt中
|
||||
allNodesApi.forEach(node => {
|
||||
originalModesApi[node.id] = node.mode;
|
||||
node.mode = LiteGraph.ALWAYS;
|
||||
});
|
||||
|
||||
let fullApiPrompt;
|
||||
try {
|
||||
// 生成完整的API prompt
|
||||
const { output } = await app.graphToPrompt();
|
||||
fullApiPrompt = output;
|
||||
console.log(`[CCXGroupExecutorSender] API prompt 生成完成,包含 ${Object.keys(fullApiPrompt).length} 个节点`);
|
||||
console.log(`[CCXGroupExecutorSender] API prompt 包含的节点 ID:`, Object.keys(fullApiPrompt));
|
||||
|
||||
// 3. 发送给后端
|
||||
console.log(`[CCXGroupExecutorSender] 发送后台执行请求到 /ccx_group_executor/execute_backend`);
|
||||
const response = await api.fetchApi('/ccx_group_executor/execute_backend', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
node_id: this.id,
|
||||
execution_list: enrichedExecutionList,
|
||||
api_prompt: fullApiPrompt
|
||||
})
|
||||
});
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error(`[CCXGroupExecutorSender] 服务器返回错误 ${response.status}:`, text);
|
||||
throw new Error(`服务器错误 ${response.status}: ${text.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === "success") {
|
||||
console.log(`[CCXGroupExecutorSender] 后台执行已成功启动`);
|
||||
return true;
|
||||
} else {
|
||||
console.error(`[CCXGroupExecutorSender] 后台执行启动失败:`, result.message);
|
||||
throw new Error(result.message || "后台执行启动失败");
|
||||
}
|
||||
} finally {
|
||||
// 无论成功还是失败,都恢复所有节点的原始模式
|
||||
allNodesApi.forEach(node => {
|
||||
if (originalModesApi[node.id] !== undefined) {
|
||||
node.mode = originalModesApi[node.id];
|
||||
}
|
||||
});
|
||||
console.log(`[CCXGroupExecutorSender] 已恢复所有节点的原始模式(API prompt生成阶段)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CCXGroupExecutorSender] 后台执行失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// 无论执行成功还是失败,都重置执行状态
|
||||
this.properties.isExecuting = false;
|
||||
this.properties.isCancelling = false;
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.getQueueStatus = async function() {
|
||||
try {
|
||||
const response = await api.fetchApi('/queue');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const queueRunning = data.queue_running || [];
|
||||
const queuePending = data.queue_pending || [];
|
||||
|
||||
return {
|
||||
isRunning: queueRunning.length > 0,
|
||||
isPending: queuePending.length > 0,
|
||||
runningCount: queueRunning.length,
|
||||
pendingCount: queuePending.length,
|
||||
rawRunning: queueRunning,
|
||||
rawPending: queuePending
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[CCXGroupExecutorSender] 获取队列状态失败:', error);
|
||||
|
||||
// 修复:在获取队列状态失败时,不要默认返回队列空闲,而是假设队列可能正在运行
|
||||
// 这样可以避免任务提交过快导致的重复Job问题
|
||||
return {
|
||||
isRunning: true,
|
||||
isPending: true,
|
||||
runningCount: 1,
|
||||
pendingCount: 0,
|
||||
rawRunning: [],
|
||||
rawPending: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
nodeType.prototype.waitForQueue = async function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkQueue = async () => {
|
||||
try {
|
||||
if (this.properties.isCancelling) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await this.getQueueStatus();
|
||||
|
||||
// 确保队列完全空闲:没有正在运行的任务,也没有待处理的任务
|
||||
if (!status.isRunning && !status.isPending) {
|
||||
// 添加一个小延迟,确保队列状态完全更新
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更频繁地检查队列状态,确保及时响应
|
||||
setTimeout(checkQueue, 300);
|
||||
} catch (error) {
|
||||
console.warn(`[CCXGroupExecutorSender] 检查队列状态失败:`, error);
|
||||
// 即使检查队列状态失败,也继续尝试,避免卡住
|
||||
setTimeout(checkQueue, 300);
|
||||
}
|
||||
};
|
||||
|
||||
checkQueue();
|
||||
});
|
||||
};
|
||||
|
||||
nodeType.prototype.cancelExecution = async function() {
|
||||
if (!this.properties.isExecuting) {
|
||||
console.warn('[CCXGroupExecutorSender] 没有正在执行的任务');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.properties.isCancelling = true;
|
||||
this.updateStatus("正在取消执行...");
|
||||
|
||||
await fetch('/interrupt', { method: 'POST' });
|
||||
|
||||
this.updateStatus("已取消");
|
||||
setTimeout(() => this.resetStatus(), 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CCXGroupExecutorSender] 取消执行时出错:', error);
|
||||
this.updateStatus(`取消失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 确保事件监听器只被注册一次
|
||||
if (!eventListenersRegistered) {
|
||||
// 包装fetchApi以捕获中断请求
|
||||
const originalFetchApi = api.fetchApi;
|
||||
api.fetchApi = async function(url, options = {}) {
|
||||
if (url === '/interrupt') {
|
||||
api.dispatchEvent(new CustomEvent("execution_interrupt", {
|
||||
detail: { timestamp: Date.now() }
|
||||
}));
|
||||
}
|
||||
|
||||
return originalFetchApi.call(this, url, options);
|
||||
};
|
||||
|
||||
// 中断请求监听器
|
||||
api.addEventListener("execution_interrupt", () => {
|
||||
const senderNodes = app.graph._nodes.filter(n =>
|
||||
n.type === "CCXGroupExecutorSender" && n.properties.isExecuting
|
||||
);
|
||||
|
||||
senderNodes.forEach(node => {
|
||||
if (node.properties.isExecuting && !node.properties.isCancelling) {
|
||||
console.log(`[CCXGroupExecutorSender] 接收到中断请求,取消节点执行:`, node.id);
|
||||
node.properties.isCancelling = true;
|
||||
node.updateStatus("正在取消执行...");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 前端执行模式的事件监听
|
||||
api.addEventListener("ccx_execute_group_list", async ({ detail }) => {
|
||||
if (!detail || !detail.node_id || !Array.isArray(detail.execution_list)) {
|
||||
console.error('[CCXGroupExecutorSender] 收到无效的执行数据:', detail);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = app.graph._nodes_by_id[detail.node_id];
|
||||
if (!node) {
|
||||
console.error(`[CCXGroupExecutorSender] 未找到节点: ${detail.node_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:检查节点是否已经在执行中,如果是则直接返回,避免重复执行
|
||||
if (node.properties.isExecuting) {
|
||||
console.warn('[CCXGroupExecutorSender] 节点已经在执行中,拒绝重复执行请求');
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:检查全局执行锁,如果有其他执行请求正在处理,则拒绝此请求
|
||||
if (globalExecutionLock) {
|
||||
console.warn('[CCXGroupExecutorSender] 全局执行锁已锁定,拒绝重复执行请求');
|
||||
app.ui.dialog.show('已有执行任务正在进行中,请等待当前任务完成后再重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置全局执行锁
|
||||
globalExecutionLock = true;
|
||||
|
||||
try {
|
||||
const executionList = detail.execution_list;
|
||||
console.log(`[CCXGroupExecutorSender] 收到执行列表:`, executionList);
|
||||
|
||||
// 检查节点是否已经在执行中,如果是则直接返回,避免重复执行
|
||||
if (node.properties.isExecuting) {
|
||||
console.warn('[CCXGroupExecutorSender] 节点已经在执行中,拒绝重复执行请求');
|
||||
return;
|
||||
}
|
||||
|
||||
node.properties.isExecuting = true;
|
||||
node.properties.isCancelling = false;
|
||||
|
||||
// 计算执行项数量,考虑重复次数,以便生成正确的JOB序号
|
||||
let totalTasks = executionList.reduce((total, item) => {
|
||||
if (item.group_name !== "__delay__" && item.group_name) {
|
||||
return total + (parseInt(item.repeat_count) || 1);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
let currentTask = 0;
|
||||
|
||||
try {
|
||||
for (const execution of executionList) {
|
||||
if (node.properties.isCancelling) {
|
||||
console.log('[CCXGroupExecutorSender] 执行被取消');
|
||||
break;
|
||||
}
|
||||
|
||||
const group_name = execution.group_name || '';
|
||||
const repeat_count = parseInt(execution.repeat_count) || 1;
|
||||
const delay_seconds = parseFloat(execution.delay_seconds) || 0;
|
||||
|
||||
if (!group_name) {
|
||||
console.warn('[CCXGroupExecutorSender] 跳过无效的组名称:', execution);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (group_name === "__delay__") {
|
||||
if (delay_seconds > 0 && !node.properties.isCancelling) {
|
||||
node.updateStatus(
|
||||
`等待下一组 ${delay_seconds}s...`
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, delay_seconds * 1000));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 为每次重复执行生成一个唯一的JOB类目标签
|
||||
for (let repeat_index = 0; repeat_index < repeat_count; repeat_index++) {
|
||||
if (node.properties.isCancelling) {
|
||||
console.log('[CCXGroupExecutorSender] 执行被取消');
|
||||
break;
|
||||
}
|
||||
|
||||
currentTask++;
|
||||
const progress = (currentTask / totalTasks) * 100;
|
||||
// 使用更清晰的任务标识,避免与后端Job编号混淆
|
||||
node.updateStatus(
|
||||
`执行组: ${group_name} (${currentTask}/${totalTasks})`,
|
||||
progress
|
||||
);
|
||||
|
||||
try {
|
||||
const outputNodes = node.getGroupOutputNodes(group_name);
|
||||
if (!outputNodes || !outputNodes.length) {
|
||||
throw new Error(`组 "${group_name}" 中没有找到输出节点`);
|
||||
}
|
||||
|
||||
const nodeIds = outputNodes.map(n => n.id);
|
||||
|
||||
try {
|
||||
if (node.properties.isCancelling) {
|
||||
break;
|
||||
}
|
||||
await queueManager.queueOutputNodes(nodeIds);
|
||||
await node.waitForQueue();
|
||||
} catch (queueError) {
|
||||
if (node.properties.isCancelling) {
|
||||
break;
|
||||
}
|
||||
console.warn(`[CCXGroupExecutorSender] 队列执行失败,使用默认方式:`, queueError);
|
||||
for (const n of outputNodes) {
|
||||
if (node.properties.isCancelling) {
|
||||
break;
|
||||
}
|
||||
if (n.triggerQueue) {
|
||||
await n.triggerQueue();
|
||||
await node.waitForQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delay_seconds > 0 && (repeat_index < repeat_count - 1 || currentTask < totalTasks) && !node.properties.isCancelling) {
|
||||
node.updateStatus(
|
||||
`执行组: ${group_name} (${currentTask}/${totalTasks}) - 等待 ${delay_seconds}s`,
|
||||
progress
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, delay_seconds * 1000));
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`执行组 "${group_name}" 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties.isCancelling) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties.isCancelling) {
|
||||
node.updateStatus("已取消");
|
||||
setTimeout(() => node.resetStatus(), 2000);
|
||||
} else {
|
||||
node.updateStatus(`执行完成 (${totalTasks}/${totalTasks})`, 100);
|
||||
setTimeout(() => node.resetStatus(), 2000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CCXGroupExecutorSender] 执行错误:', error);
|
||||
node.updateStatus(`错误: ${error.message}`);
|
||||
app.ui.dialog.show(`执行错误: ${error.message}`);
|
||||
} finally {
|
||||
node.properties.isExecuting = false;
|
||||
node.properties.isCancelling = false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[CCXGroupExecutorSender] 执行失败:`, error);
|
||||
app.ui.dialog.show(`执行错误: ${error.message}`);
|
||||
node.updateStatus(`错误: ${error.message}`);
|
||||
node.properties.isExecuting = false;
|
||||
node.properties.isCancelling = false;
|
||||
} finally {
|
||||
// 释放全局执行锁
|
||||
globalExecutionLock = false;
|
||||
console.log('[CCXGroupExecutorSender] 全局执行锁已释放');
|
||||
}
|
||||
});
|
||||
|
||||
// 后台执行模式的事件监听
|
||||
api.addEventListener("ccx_execute_group_list_backend", async ({ detail }) => {
|
||||
if (!detail || !detail.node_id || !Array.isArray(detail.execution_list)) {
|
||||
console.error('[CCXGroupExecutorSender] 收到无效的后台执行数据:', detail);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = app.graph._nodes_by_id[detail.node_id];
|
||||
if (!node) {
|
||||
console.error(`[CCXGroupExecutorSender] 未找到节点: ${detail.node_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:检查全局执行锁,如果有其他执行请求正在处理,则拒绝此请求
|
||||
if (globalExecutionLock) {
|
||||
console.warn('[CCXGroupExecutorSender] 全局执行锁已锁定,拒绝重复执行请求');
|
||||
app.ui.dialog.show('已有执行任务正在进行中,请等待当前任务完成后再重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置全局执行锁
|
||||
globalExecutionLock = true;
|
||||
|
||||
try {
|
||||
console.log(`[CCXGroupExecutorSender] 收到后台执行请求:`, detail.execution_list);
|
||||
await node.executeInBackend(detail.execution_list);
|
||||
} catch (error) {
|
||||
console.error(`[CCXGroupExecutorSender] 后台执行失败:`, error);
|
||||
app.ui.dialog.show(`执行错误: ${error.message}`);
|
||||
if (node.updateStatus) {
|
||||
node.updateStatus(`错误: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
// 释放全局执行锁
|
||||
globalExecutionLock = false;
|
||||
console.log('[CCXGroupExecutorSender] 全局执行锁已释放');
|
||||
}
|
||||
});
|
||||
|
||||
// 标记所有事件监听器已注册
|
||||
eventListenersRegistered = true;
|
||||
console.log(`[CCXGroupExecutorSender] 所有事件监听器已注册`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
class EventManager {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
addEventListener(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
removeEventListener(event, callback) {
|
||||
if (this.listeners.has(event)) {
|
||||
const callbacks = this.listeners.get(event);
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(event, detail = {}) {
|
||||
if (this.listeners.has(event)) {
|
||||
const callbacks = this.listeners.get(event);
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback({ detail });
|
||||
} catch (error) {
|
||||
console.error(`Error in event listener for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class QueueManager {
|
||||
constructor() {
|
||||
this.eventManager = new EventManager();
|
||||
this.queueNodeIds = null;
|
||||
this.processingQueue = false;
|
||||
this.lastAdjustedMouseEvent = null;
|
||||
this.initializeHooks();
|
||||
}
|
||||
|
||||
initializeHooks() {
|
||||
const originalQueuePrompt = app.queuePrompt;
|
||||
const originalGraphToPrompt = app.graphToPrompt;
|
||||
const originalApiQueuePrompt = api.queuePrompt;
|
||||
|
||||
app.queuePrompt = async function() {
|
||||
queueManager.processingQueue = true;
|
||||
queueManager.eventManager.dispatchEvent("queue");
|
||||
try {
|
||||
await originalQueuePrompt.apply(app, [...arguments]);
|
||||
} finally {
|
||||
queueManager.processingQueue = false;
|
||||
queueManager.eventManager.dispatchEvent("queue-end");
|
||||
}
|
||||
};
|
||||
|
||||
app.graphToPrompt = async function() {
|
||||
queueManager.eventManager.dispatchEvent("graph-to-prompt");
|
||||
let promise = originalGraphToPrompt.apply(app, [...arguments]);
|
||||
await promise;
|
||||
queueManager.eventManager.dispatchEvent("graph-to-prompt-end");
|
||||
return promise;
|
||||
};
|
||||
|
||||
api.queuePrompt = async function(index, prompt) {
|
||||
// 创建局部副本,避免异步执行时queueNodeIds被重置导致数据丢失
|
||||
const localQueueNodeIds = [...(queueManager.queueNodeIds || [])];
|
||||
|
||||
// 修复:检测到有 CCXGroupExecutorSender 输出节点时,只保留它
|
||||
if (prompt.output && prompt.nodes) {
|
||||
let hasSenderNode = false;
|
||||
let senderNodeId = null;
|
||||
|
||||
for (const outputNodeId in prompt.output) {
|
||||
const nodeData = prompt.nodes[outputNodeId];
|
||||
if (nodeData && nodeData.class_type === "CCXGroupExecutorSender") {
|
||||
hasSenderNode = true;
|
||||
senderNodeId = outputNodeId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 只在没有明确指定输出节点ID时才进行此修改
|
||||
// 避免与CCXGroupExecutor的自定义执行逻辑冲突
|
||||
if (hasSenderNode && senderNodeId && !localQueueNodeIds.length) {
|
||||
console.log(`[QueueManager] 检测到 CCXGroupExecutorSender 节点,只保留它作为输出节点`);
|
||||
// 只保留 sender 节点
|
||||
const oldOutput = prompt.output;
|
||||
prompt.output = {};
|
||||
prompt.output[senderNodeId] = oldOutput[senderNodeId];
|
||||
}
|
||||
}
|
||||
|
||||
if (localQueueNodeIds.length && prompt.output) {
|
||||
const oldOutput = prompt.output;
|
||||
let newOutput = {};
|
||||
for (const queueNodeId of localQueueNodeIds) {
|
||||
queueManager.recursiveAddNodes(String(queueNodeId), oldOutput, newOutput);
|
||||
}
|
||||
prompt.output = newOutput;
|
||||
}
|
||||
queueManager.eventManager.dispatchEvent("comfy-api-queue-prompt-before", {
|
||||
workflow: prompt.workflow,
|
||||
output: prompt.output,
|
||||
});
|
||||
const response = originalApiQueuePrompt.apply(api, [index, prompt]);
|
||||
queueManager.eventManager.dispatchEvent("comfy-api-queue-prompt-end");
|
||||
return response;
|
||||
};
|
||||
|
||||
const originalProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
|
||||
const originalAdjustMouseEvent = LGraphCanvas.prototype.adjustMouseEvent;
|
||||
const originalProcessMouseMove = LGraphCanvas.prototype.processMouseMove;
|
||||
|
||||
LGraphCanvas.prototype.processMouseDown = function(e) {
|
||||
const result = originalProcessMouseDown.apply(this, [...arguments]);
|
||||
queueManager.lastAdjustedMouseEvent = e;
|
||||
return result;
|
||||
};
|
||||
|
||||
LGraphCanvas.prototype.adjustMouseEvent = function(e) {
|
||||
originalAdjustMouseEvent.apply(this, [...arguments]);
|
||||
queueManager.lastAdjustedMouseEvent = e;
|
||||
};
|
||||
|
||||
LGraphCanvas.prototype.processMouseMove = function(e) {
|
||||
const result = originalProcessMouseMove.apply(this, [...arguments]);
|
||||
if (e && !e.canvasX && !e.canvasY) {
|
||||
const canvas = app.canvas;
|
||||
const offset = canvas.convertEventToCanvasOffset(e);
|
||||
e.canvasX = offset[0];
|
||||
e.canvasY = offset[1];
|
||||
}
|
||||
queueManager.lastAdjustedMouseEvent = e;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
recursiveAddNodes(nodeId, oldOutput, newOutput) {
|
||||
let currentId = nodeId;
|
||||
let currentNode = oldOutput[currentId];
|
||||
if (!currentNode) {
|
||||
console.warn(`[QueueManager] 节点 ${currentId} 在输出中不存在`);
|
||||
return newOutput;
|
||||
}
|
||||
if (newOutput[currentId] == null) {
|
||||
newOutput[currentId] = currentNode;
|
||||
for (const inputValue of Object.values(currentNode.inputs || [])) {
|
||||
if (Array.isArray(inputValue)) {
|
||||
this.recursiveAddNodes(inputValue[0], oldOutput, newOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newOutput;
|
||||
}
|
||||
async queueOutputNodes(nodeIds) {
|
||||
// 如果队列正在处理中,等待它完成后再继续
|
||||
while (this.processingQueue) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
try {
|
||||
this.queueNodeIds = nodeIds;
|
||||
this.processingQueue = true;
|
||||
this.eventManager.dispatchEvent("queue");
|
||||
await app.queuePrompt();
|
||||
} catch (e) {
|
||||
console.error("队列节点时出错:", e);
|
||||
} finally {
|
||||
this.queueNodeIds = null;
|
||||
this.processingQueue = false;
|
||||
this.eventManager.dispatchEvent("queue-end");
|
||||
}
|
||||
}
|
||||
getLastMouseEvent() {
|
||||
return this.lastAdjustedMouseEvent;
|
||||
}
|
||||
addEventListener(event, callback) {
|
||||
this.eventManager.addEventListener(event, callback);
|
||||
}
|
||||
removeEventListener(event, callback) {
|
||||
this.eventManager.removeEventListener(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function getOutputNodes(nodes) {
|
||||
return (nodes?.filter((n) => {
|
||||
return (n.mode != LiteGraph.NEVER &&
|
||||
n.constructor.nodeData?.output_node);
|
||||
}) || []);
|
||||
}
|
||||
const queueManager = new QueueManager();
|
||||
function queueSelectedOutputNodes() {
|
||||
const selectedNodes = app.canvas.selected_nodes;
|
||||
if (!selectedNodes || Object.keys(selectedNodes).length === 0) {
|
||||
console.log("[LG]队列: 没有选中的节点");
|
||||
return;
|
||||
}
|
||||
|
||||
const outputNodes = getOutputNodes(Object.values(selectedNodes));
|
||||
if (!outputNodes || outputNodes.length === 0) {
|
||||
console.log("[LG]队列: 选中的节点中没有输出节点");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[LG]队列: 执行 ${outputNodes.length} 个输出节点`);
|
||||
queueManager.queueOutputNodes(outputNodes.map((n) => n.id));
|
||||
}
|
||||
|
||||
function queueGroupOutputNodes() {
|
||||
const lastMouseEvent = queueManager.getLastMouseEvent();
|
||||
if (!lastMouseEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvasX = lastMouseEvent.canvasX;
|
||||
let canvasY = lastMouseEvent.canvasY;
|
||||
|
||||
if (!canvasX || !canvasY) {
|
||||
const canvas = app.canvas;
|
||||
const mousePos = canvas.getMousePos();
|
||||
canvasX = mousePos[0];
|
||||
canvasY = mousePos[1];
|
||||
}
|
||||
|
||||
const group = app.graph.getGroupOnPos(canvasX, canvasY);
|
||||
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
group.recomputeInsideNodes();
|
||||
|
||||
if (!group._nodes || group._nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputNodes = getOutputNodes(group._nodes);
|
||||
if (!outputNodes || outputNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
queueManager.queueOutputNodes(outputNodes.map((n) => n.id));
|
||||
}
|
||||
|
||||
export { queueManager, getOutputNodes, queueSelectedOutputNodes, queueGroupOutputNodes };
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,799 @@
|
||||
{
|
||||
"id": "929794be-060e-495f-a15e-e6de166f672a",
|
||||
"revision": 0,
|
||||
"last_node_id": 45,
|
||||
"last_link_id": 25,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 25,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
1105.8806682095721,
|
||||
-1897.3221486615937
|
||||
],
|
||||
"size": [
|
||||
388.82975725321694,
|
||||
68.10032247019217
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "base_directory"
|
||||
},
|
||||
"links": [
|
||||
15
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "1.Photoshop安装目录",
|
||||
"properties": {
|
||||
"Run widget replace on values": false,
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"color": "#332922",
|
||||
"bgcolor": "#593930"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "CreateSDPPPInstallationDirectory",
|
||||
"pos": [
|
||||
1539.534326532326,
|
||||
-1904.703005664239
|
||||
],
|
||||
"size": [
|
||||
325.5474609375,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "base_directory",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "base_directory"
|
||||
},
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
10
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "WWWEN8/ComfyUI-CCXManager",
|
||||
"ver": "e063d1d7c2dd20baecc40924ea92c02888151d13",
|
||||
"Node name for S&R": "CreateSDPPPInstallationDirectory",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"type": "CCXRepoUpdaterNode",
|
||||
"pos": [
|
||||
1405.429467043008,
|
||||
-2153.158961958952
|
||||
],
|
||||
"size": [
|
||||
348.1265625,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "auto_update",
|
||||
"type": "BOOLEAN",
|
||||
"widget": {
|
||||
"name": "auto_update"
|
||||
},
|
||||
"link": 20
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "WWWEN8/ComfyUI-CCXManager",
|
||||
"ver": "e063d1d7c2dd20baecc40924ea92c02888151d13",
|
||||
"Node name for S&R": "CCXRepoUpdaterNode",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"https://github.com/zombieyang/sd-ppp.git",
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"type": "ShowText|pysssss",
|
||||
"pos": [
|
||||
1849.8495180833402,
|
||||
-2120.661846701842
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"label": "文本",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "字符串",
|
||||
"name": "字符串",
|
||||
"shape": 6,
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": ".展示文本",
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-custom-scripts",
|
||||
"ver": "1.2.5",
|
||||
"Node name for S&R": "ShowText|pysssss",
|
||||
"widget_ue_connectable": {},
|
||||
"sdppp_widgetable_title": ".展示文本"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "ShowText|pysssss",
|
||||
"pos": [
|
||||
1929.103263038,
|
||||
-1890.5868117086268
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
76
|
||||
],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"label": "文本",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "字符串",
|
||||
"name": "字符串",
|
||||
"shape": 6,
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": ".7.展示文本",
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-custom-scripts",
|
||||
"ver": "1.2.5",
|
||||
"Node name for S&R": "ShowText|pysssss",
|
||||
"widget_ue_connectable": {},
|
||||
"sdppp_widgetable_title": ".7.展示文本"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
1131.1834964411892,
|
||||
-1364.5450730318003
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "COMBO",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "auto_run_on_restart"
|
||||
},
|
||||
"links": [
|
||||
14
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "4.SDPPP2.0侧自动",
|
||||
"properties": {
|
||||
"Run widget replace on values": false,
|
||||
"sdppp_widgetable_title": "4.SDPPP2.0侧自动",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
"fixed",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "ShowText|pysssss",
|
||||
"pos": [
|
||||
1916.8164672469622,
|
||||
-1579.8334453592367
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"label": "文本",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "字符串",
|
||||
"name": "字符串",
|
||||
"shape": 6,
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": ".8.展示文本",
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-custom-scripts",
|
||||
"ver": "1.2.5",
|
||||
"Node name for S&R": "ShowText|pysssss",
|
||||
"widget_ue_connectable": {},
|
||||
"sdppp_widgetable_title": ".8.展示文本"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "ShowText|pysssss",
|
||||
"pos": [
|
||||
1906.0676082607115,
|
||||
-1322.9794900336842
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"label": "文本",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 12
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "字符串",
|
||||
"name": "字符串",
|
||||
"shape": 6,
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": ".9.展示文本",
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-custom-scripts",
|
||||
"ver": "1.2.5",
|
||||
"Node name for S&R": "ShowText|pysssss",
|
||||
"widget_ue_connectable": {},
|
||||
"sdppp_widgetable_title": ".9.展示文本"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"type": "Fast Groups Bypasser (rgthree)",
|
||||
"pos": [
|
||||
2191.9729478322884,
|
||||
-1910.6146573779874
|
||||
],
|
||||
"size": [
|
||||
284.2597733388643,
|
||||
130
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"label": "可选连接",
|
||||
"name": "OPT_CONNECTION",
|
||||
"type": "*",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": "0.忽略多框",
|
||||
"properties": {
|
||||
"matchColors": "",
|
||||
"matchTitle": "",
|
||||
"showNav": true,
|
||||
"showAllGraphs": true,
|
||||
"sort": "position",
|
||||
"customSortAlphabet": "",
|
||||
"toggleRestriction": "default",
|
||||
"widget_ue_connectable": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"type": "CCXGroupExecutorSender",
|
||||
"pos": [
|
||||
469.76881635393283,
|
||||
-1660.4222939618908
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
330.09755836698196
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "signal",
|
||||
"type": "SIGNAL",
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"aux_id": "WWWEN8/ComfyUI-CCXManager",
|
||||
"ver": "f6f40005df22aab6e86ccb33aa40b383d5f643ac",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"前端执行"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
1129.1369478659683,
|
||||
-2136.280137293815
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
60
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "BOOLEAN",
|
||||
"type": "BOOLEAN",
|
||||
"widget": {
|
||||
"name": "auto_update"
|
||||
},
|
||||
"links": [
|
||||
20
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "2.Comfyui侧自动",
|
||||
"properties": {
|
||||
"Run widget replace on values": false,
|
||||
"sdppp_widgetable_title": "2.Comfyui侧自动",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
1128.1663149315525,
|
||||
-1603.2745281963978
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "COMBO",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "auto_run_on_restart"
|
||||
},
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "3.SDPPP1.0侧自动",
|
||||
"properties": {
|
||||
"Run widget replace on values": false,
|
||||
"sdppp_widgetable_title": "3.SDPPP1.0侧自动",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
"fixed",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"type": "Note",
|
||||
"pos": [
|
||||
2212.5814921432616,
|
||||
-1607.2801786881887
|
||||
],
|
||||
"size": [
|
||||
284.4355320312786,
|
||||
500.8837467522296
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"title": "#5.使用说明",
|
||||
"properties": {
|
||||
"widget_ue_connectable": {},
|
||||
"sdppp_widgetable_title": "#5.使用说明"
|
||||
},
|
||||
"widgets_values": [
|
||||
"## 节点说明:\n设置运行一次,以后SD-PPP(Photoshop侧和Comfyui侧)有更新情况,自动帮您同步更新,完全摆脱手动去更新插件和节点了。(最新功能:没有安装过SD-PPP小伙伴有福了,运行此工作流自动全部安装和以后维护更新)\n\n## 使用方法:\n\n1.之前安装过Photoshop侧SD-PPP2.0,可以先在Comfyui侧保存好这个工作流,重启后在Photoshop侧打开这个工作流操作(不需在Photoshop侧运行,忽略这条信息)\n\n2.Photoshop安装目录输入:<photoshop安装目录>\n例如:C:\\Program Files\\Adobe\\Adobe Photoshop 2025\n\n3.“Comfyui侧自动”和“SDPPP1.0侧自动”,“SDPPP1.0侧自动”启动Comfyui是否开启自动更新功能,默认开启;想关闭选择对应项即可,重新运行生效。\n\n4.点击运行\n\n5.之前有安装过Photoshop侧的文件夹请删掉\n\n6.查看控制台有没成功运行信息\n\n7.Comfyui侧没显示更新,Comfyui侧重启即可\n\n详情请阅读:https://github.com/WWWEN8/ComfyUI-CCXManager"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"type": "CCXGroupExecutorSingle",
|
||||
"pos": [
|
||||
476.6257437918548,
|
||||
-1949.1960657634113
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "signal",
|
||||
"shape": 7,
|
||||
"type": "SIGNAL",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "SIGNAL",
|
||||
"type": "SIGNAL",
|
||||
"links": [
|
||||
25
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "WWWEN8/ComfyUI-CCXManager",
|
||||
"ver": "f6f40005df22aab6e86ccb33aa40b383d5f643ac",
|
||||
"Node name for S&R": "CCXGroupExecutorSingle",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"Comfyui侧安装开关\n自动创建安装目录\n启动时自动安装开关",
|
||||
1,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "CCXManagerCopy",
|
||||
"pos": [
|
||||
1463.2306485611005,
|
||||
-1610.50782412996
|
||||
],
|
||||
"size": [
|
||||
378.5259765625,
|
||||
150
|
||||
],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "auto_run_on_restart",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "auto_run_on_restart"
|
||||
},
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
11
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Generated_Path",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "WWWEN8/ComfyUI-CCXManager",
|
||||
"ver": "e063d1d7c2dd20baecc40924ea92c02888151d13",
|
||||
"Node name for S&R": "CCXManagerCopy",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"local_path",
|
||||
"",
|
||||
"https://github.com/zombieyang/sd-ppp.git",
|
||||
"enable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "CCXManager",
|
||||
"pos": [
|
||||
1468.1519500538484,
|
||||
-1390.970393645639
|
||||
],
|
||||
"size": [
|
||||
378.5259765625,
|
||||
150
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "auto_run_on_restart",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "auto_run_on_restart"
|
||||
},
|
||||
"link": 14
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
12
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Generated_Path",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "WWWEN8/ComfyUI-CCXManager",
|
||||
"ver": "e063d1d7c2dd20baecc40924ea92c02888151d13",
|
||||
"Node name for S&R": "CCXManager",
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"url",
|
||||
"https://gitee.com/zombieyang/sd-ppp/raw/main/static/sd-ppp2_PS.ccx",
|
||||
"https://github.com/zombieyang/sd-ppp.git",
|
||||
"enable"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
10,
|
||||
16,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
11,
|
||||
18,
|
||||
0,
|
||||
19,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
12,
|
||||
20,
|
||||
0,
|
||||
21,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
13,
|
||||
23,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"COMBO"
|
||||
],
|
||||
[
|
||||
14,
|
||||
24,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"COMBO"
|
||||
],
|
||||
[
|
||||
15,
|
||||
25,
|
||||
0,
|
||||
16,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
20,
|
||||
37,
|
||||
0,
|
||||
38,
|
||||
0,
|
||||
"BOOLEAN"
|
||||
],
|
||||
[
|
||||
21,
|
||||
38,
|
||||
0,
|
||||
39,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
25,
|
||||
45,
|
||||
0,
|
||||
44,
|
||||
0,
|
||||
"SIGNAL"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "启动时自动安装开关",
|
||||
"bounding": [
|
||||
1118.1663149315525,
|
||||
-1684.10782412996,
|
||||
948.6501523154095,
|
||||
472.39672906550925
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "使用说明",
|
||||
"bounding": [
|
||||
2202.5814921432616,
|
||||
-1680.8801786881886,
|
||||
304.4355320312784,
|
||||
584.4837467522296
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "自动创建安装目录",
|
||||
"bounding": [
|
||||
1095.8806682095721,
|
||||
-1978.303005664239,
|
||||
983.2225948284279,
|
||||
173.71619395561228
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Comfyui侧安装开关",
|
||||
"bounding": [
|
||||
1119.1369478659683,
|
||||
-2226.758961958952,
|
||||
880.7125702173719,
|
||||
189.6
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ue_links": [],
|
||||
"links_added_by_ue": [],
|
||||
"ds": {
|
||||
"scale": 0.724729500000001,
|
||||
"offset": [
|
||||
119.59095384660509,
|
||||
2329.0051627829334
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.38.13",
|
||||
"workflowRendererVersion": "LG",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user