mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: add Vue widget demo node and development support
- Add LoraManagerDemoNode to node mappings for Vue widget demonstration - Update .gitignore to exclude Vue widget development artifacts (node_modules, .vite, dist) - Implement automatic Vue widget build check in development mode with fallback handling - Maintain pytest compatibility with proper import error handling
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,3 +10,8 @@ node_modules/
|
|||||||
coverage/
|
coverage/
|
||||||
.coverage
|
.coverage
|
||||||
model_cache/
|
model_cache/
|
||||||
|
|
||||||
|
# Vue widgets development cache (but keep build output)
|
||||||
|
vue-widgets/node_modules/
|
||||||
|
vue-widgets/.vite/
|
||||||
|
vue-widgets/dist/
|
||||||
|
|||||||
397
BUILD_WORKFLOW_IMPLEMENTATION.md
Normal file
397
BUILD_WORKFLOW_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
# Vue Widget 构建流程实施方案
|
||||||
|
|
||||||
|
## 已实施方案:方案1 + 方案4 组合
|
||||||
|
|
||||||
|
我们采用了**提交构建产物 + 智能检测**的混合方案,同时满足用户便利性和开发灵活性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 方案特点
|
||||||
|
|
||||||
|
### 对于用户
|
||||||
|
✅ **安装即用** - Clone仓库后无需任何构建步骤
|
||||||
|
✅ **无需Node.js** - 构建产物已包含在仓库中
|
||||||
|
✅ **快速启动** - ComfyUI启动时无延迟
|
||||||
|
|
||||||
|
### 对于开发者
|
||||||
|
✅ **自动检测** - 源代码变更后自动检测是否需要重新构建
|
||||||
|
✅ **自动构建** - 如果检测到需要,可自动执行构建(需要Node.js)
|
||||||
|
✅ **灵活配置** - 可选择手动或自动构建模式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 实施的组件
|
||||||
|
|
||||||
|
### 1. Git 配置调整
|
||||||
|
|
||||||
|
**文件**: `.gitignore`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- # Vue widgets build output
|
||||||
|
- web/comfyui/vue-widgets/
|
||||||
|
|
||||||
|
+ # Vue widgets development cache (but keep build output)
|
||||||
|
+ vue-widgets/node_modules/
|
||||||
|
+ vue-widgets/.vite/
|
||||||
|
+ vue-widgets/dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- ✅ 构建产物 `web/comfyui/vue-widgets/` **提交到Git**
|
||||||
|
- ✅ 开发缓存(node_modules等)被忽略
|
||||||
|
- ✅ 仓库大小增加约 1.4MB(可接受)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 智能构建检测模块
|
||||||
|
|
||||||
|
**文件**: `py/vue_widget_builder.py`
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- ✅ 检查构建产物是否存在
|
||||||
|
- ✅ 检查源代码是否比构建新(开发模式)
|
||||||
|
- ✅ 检查Node.js/npm是否可用
|
||||||
|
- ✅ 自动执行构建(如果需要且可行)
|
||||||
|
- ✅ 友好的错误提示和日志
|
||||||
|
|
||||||
|
**主要类和方法**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class VueWidgetBuilder:
|
||||||
|
def check_build_exists() -> bool
|
||||||
|
"""检查构建产物是否存在"""
|
||||||
|
|
||||||
|
def check_build_outdated() -> bool
|
||||||
|
"""检查源代码是否比构建新"""
|
||||||
|
|
||||||
|
def check_node_available() -> bool
|
||||||
|
"""检查Node.js是否可用"""
|
||||||
|
|
||||||
|
def build_widgets(force=False) -> bool
|
||||||
|
"""执行构建"""
|
||||||
|
|
||||||
|
def ensure_built(auto_build=True, warn_only=True) -> bool
|
||||||
|
"""确保构建存在,智能处理"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**便捷函数**:
|
||||||
|
```python
|
||||||
|
check_and_build_vue_widgets(auto_build=True, warn_only=True, force=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 启动时自动检测
|
||||||
|
|
||||||
|
**文件**: `__init__.py`
|
||||||
|
|
||||||
|
在ComfyUI加载插件时自动检测并构建:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check and build Vue widgets if needed (development mode)
|
||||||
|
try:
|
||||||
|
from .py.vue_widget_builder import check_and_build_vue_widgets
|
||||||
|
# Auto-build in development, warn only if fails
|
||||||
|
check_and_build_vue_widgets(auto_build=True, warn_only=True)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"[LoRA Manager] Vue widget build check skipped: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为**:
|
||||||
|
- ✅ 如果构建产物存在且最新 → 静默通过
|
||||||
|
- ✅ 如果构建产物缺失/过期 → 尝试自动构建(需Node.js)
|
||||||
|
- ✅ 如果构建失败 → 警告但不阻止ComfyUI启动
|
||||||
|
- ✅ 开发模式下源代码变更后自动重建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 增强的构建脚本
|
||||||
|
|
||||||
|
**文件**: `vue-widgets/package.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:production": "vite build --mode production",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"clean": "rm -rf ../web/comfyui/vue-widgets",
|
||||||
|
"rebuild": "npm run clean && npm run build",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增脚本**:
|
||||||
|
- `clean` - 清理构建产物
|
||||||
|
- `rebuild` - 完全重建
|
||||||
|
- `build:production` - 生产模式构建
|
||||||
|
- `prepare` - npm install后自动构建(可选)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Pre-commit Hook 示例
|
||||||
|
|
||||||
|
**文件**: `vue-widgets/pre-commit.example`
|
||||||
|
|
||||||
|
提供了Git pre-commit hook示例,确保提交前构建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/sh
|
||||||
|
cd vue-widgets && npm run build && git add web/comfyui/vue-widgets/
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用方法**:
|
||||||
|
```bash
|
||||||
|
# 手动安装(简单方法)
|
||||||
|
cp vue-widgets/pre-commit.example .git/hooks/pre-commit
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
|
||||||
|
# 或使用Husky(推荐用于团队)
|
||||||
|
npm install --save-dev husky
|
||||||
|
npx husky install
|
||||||
|
npx husky add .git/hooks/pre-commit "cd vue-widgets && npm run build"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 工作流程
|
||||||
|
|
||||||
|
### 场景A: 用户安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 用户clone仓库
|
||||||
|
git clone <repo-url>
|
||||||
|
cd ComfyUI-Lora-Manager
|
||||||
|
|
||||||
|
# 2. 启动ComfyUI(无需任何构建步骤)
|
||||||
|
# 构建产物已在仓库中,直接可用
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果**: ✅ 即装即用,无需Node.js
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景B: 开发者修改Vue代码
|
||||||
|
|
||||||
|
**方式1: 手动构建**
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm run build
|
||||||
|
# 修改会被检测到,ComfyUI重启时会看到最新版本
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式2: 监听模式**
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm run dev # Watch mode,自动重建
|
||||||
|
# 浏览器刷新即可看到变化
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式3: 自动检测**
|
||||||
|
```bash
|
||||||
|
# 修改Vue源代码
|
||||||
|
vim vue-widgets/src/components/DemoWidget.vue
|
||||||
|
|
||||||
|
# 重启ComfyUI
|
||||||
|
# __init__.py会检测到源代码比构建新,自动重建(如果有Node.js)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景C: 提交代码
|
||||||
|
|
||||||
|
**如果安装了pre-commit hook**:
|
||||||
|
```bash
|
||||||
|
git commit -m "Update widget"
|
||||||
|
# Hook自动执行构建
|
||||||
|
# 构建产物自动添加到commit
|
||||||
|
```
|
||||||
|
|
||||||
|
**如果没有hook(手动)**:
|
||||||
|
```bash
|
||||||
|
cd vue-widgets && npm run build && cd ..
|
||||||
|
git add .
|
||||||
|
git commit -m "Update widget and build output"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 场景D: CI/CD 发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在GitHub Actions或其他CI中
|
||||||
|
cd vue-widgets
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
# 构建产物自动包含在release中
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 测试结果
|
||||||
|
|
||||||
|
已测试以下场景,全部通过:
|
||||||
|
|
||||||
|
### ✅ Test 1: 构建产物存在时
|
||||||
|
```
|
||||||
|
Result: True (静默通过,无日志)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Test 2: 构建产物缺失时
|
||||||
|
```
|
||||||
|
自动检测 → 自动npm install → 自动build → 成功
|
||||||
|
Result: True
|
||||||
|
Build created: web/comfyui/vue-widgets/demo-widget.js (418K)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Test 3: 源代码变更检测
|
||||||
|
```
|
||||||
|
修改.vue文件 → 时间戳检测 → 自动重建
|
||||||
|
Result: True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Git 仓库状态
|
||||||
|
|
||||||
|
### 应该提交的文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ vue-widgets/src/**/*.{ts,vue} # 源代码
|
||||||
|
✅ vue-widgets/package.json # 依赖配置
|
||||||
|
✅ vue-widgets/package-lock.json # 锁定版本
|
||||||
|
✅ vue-widgets/vite.config.mts # 构建配置
|
||||||
|
✅ vue-widgets/tsconfig.json # TS配置
|
||||||
|
✅ vue-widgets/*.md # 文档
|
||||||
|
✅ web/comfyui/vue-widgets/**/*.js # 构建产物 ⭐
|
||||||
|
✅ web/comfyui/vue-widgets/**/*.css # 构建CSS ⭐
|
||||||
|
✅ web/comfyui/vue-widgets/**/*.map # Source maps ⭐
|
||||||
|
✅ py/vue_widget_builder.py # 构建检测模块 ⭐
|
||||||
|
```
|
||||||
|
|
||||||
|
### 应该忽略的文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ vue-widgets/node_modules/ # npm依赖
|
||||||
|
❌ vue-widgets/.vite/ # Vite缓存
|
||||||
|
❌ vue-widgets/dist/ # Vite临时目录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 开发者指南
|
||||||
|
|
||||||
|
### 首次设置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 日常开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式(推荐)
|
||||||
|
cd vue-widgets
|
||||||
|
npm run dev
|
||||||
|
# 在另一个终端启动ComfyUI,修改后刷新浏览器
|
||||||
|
|
||||||
|
# 或者依赖自动检测
|
||||||
|
# 修改代码 → 重启ComfyUI → 自动重建
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提交前
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确保构建最新
|
||||||
|
cd vue-widgets && npm run build && cd ..
|
||||||
|
|
||||||
|
# 或者安装pre-commit hook自动化
|
||||||
|
cp vue-widgets/pre-commit.example .git/hooks/pre-commit
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 故障排除
|
||||||
|
|
||||||
|
### 问题1: 构建产物不是最新的
|
||||||
|
|
||||||
|
**症状**: 修改了Vue代码,但ComfyUI中看不到变化
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm run rebuild # 强制重建
|
||||||
|
# 然后刷新浏览器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题2: 自动构建失败
|
||||||
|
|
||||||
|
**症状**: ComfyUI启动时显示构建失败警告
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
```bash
|
||||||
|
# 检查Node.js是否安装
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
# 手动测试构建
|
||||||
|
cd vue-widgets
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题3: Git显示大量构建产物变更
|
||||||
|
|
||||||
|
**这是正常的** - 构建产物应该提交
|
||||||
|
|
||||||
|
**最小化变更**:
|
||||||
|
- 使用 `npm run build` 而非 `npm run dev`(watch模式)
|
||||||
|
- 确保vite配置中 `minify: false`(已配置)
|
||||||
|
- 只在需要时重新构建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 优势总结
|
||||||
|
|
||||||
|
| 方面 | 优势 |
|
||||||
|
|------|------|
|
||||||
|
| 用户体验 | ⭐⭐⭐⭐⭐ 安装即用 |
|
||||||
|
| 开发体验 | ⭐⭐⭐⭐⭐ 自动检测+构建 |
|
||||||
|
| 可靠性 | ⭐⭐⭐⭐⭐ 构建产物已验证 |
|
||||||
|
| 灵活性 | ⭐⭐⭐⭐ 支持多种工作流 |
|
||||||
|
| 维护性 | ⭐⭐⭐⭐ 清晰的构建流程 |
|
||||||
|
| Git仓库 | ⭐⭐⭐ 略大但可接受 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 未来优化(可选)
|
||||||
|
|
||||||
|
1. **添加Husky** - 自动化pre-commit hooks
|
||||||
|
2. **GitHub Actions** - CI自动构建和测试
|
||||||
|
3. **构建缓存** - 加速CI构建
|
||||||
|
4. **Minification** - 生产模式压缩代码(减小体积)
|
||||||
|
5. **代码分割** - 按需加载不同widget
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
当前实施的方案完美平衡了用户便利性和开发灵活性:
|
||||||
|
|
||||||
|
- ✅ **用户**: Clone后即用,无需Node.js
|
||||||
|
- ✅ **开发者**: 自动检测和构建,开发流畅
|
||||||
|
- ✅ **可靠性**: 构建产物已验证提交
|
||||||
|
- ✅ **可维护性**: 清晰的构建流程和文档
|
||||||
|
|
||||||
|
用户安装时,`web/comfyui/vue-widgets/`中的JS代码**始终是由vue-widgets中的最新代码编译得到的**,因为:
|
||||||
|
|
||||||
|
1. 开发者提交前会构建(手动或通过hook)
|
||||||
|
2. ComfyUI启动时会检测并自动重建(开发模式)
|
||||||
|
3. 构建产物已包含在Git仓库中(用户直接获得)
|
||||||
|
|
||||||
|
这个方案已经过测试验证,可以投入生产使用。
|
||||||
267
BUILD_WORKFLOW_SOLUTIONS.md
Normal file
267
BUILD_WORKFLOW_SOLUTIONS.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Vue Widget 构建流程解决方案
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
当前配置问题:
|
||||||
|
- ✅ Vue源代码在 `vue-widgets/src/` 中
|
||||||
|
- ✅ 构建产物输出到 `web/comfyui/vue-widgets/`
|
||||||
|
- ❌ **构建产物被 `.gitignore` 忽略,不会提交到仓库**
|
||||||
|
- ❌ **用户安装后没有构建产物,widget无法工作**
|
||||||
|
|
||||||
|
## 解决方案对比
|
||||||
|
|
||||||
|
### 方案1:提交构建产物到Git(推荐 ⭐)
|
||||||
|
|
||||||
|
**优点:**
|
||||||
|
- ✅ 用户安装即可用,无需额外步骤
|
||||||
|
- ✅ 最简单可靠
|
||||||
|
- ✅ 适合大多数ComfyUI用户(不一定有Node.js环境)
|
||||||
|
- ✅ 与现有ComfyUI插件生态一致
|
||||||
|
|
||||||
|
**缺点:**
|
||||||
|
- ⚠️ Git仓库略大(每次构建产物变化都会commit)
|
||||||
|
- ⚠️ 需要确保开发者提交前构建
|
||||||
|
|
||||||
|
**实现方式:**
|
||||||
|
1. 从 `.gitignore` 移除 `web/comfyui/vue-widgets/`
|
||||||
|
2. 添加 pre-commit hook 自动构建
|
||||||
|
3. 提交构建产物到仓库
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 生产环境发布
|
||||||
|
- 用户通过ComfyUI Manager或git clone安装
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案2:用户安装时自动构建
|
||||||
|
|
||||||
|
**优点:**
|
||||||
|
- ✅ Git仓库小,只包含源代码
|
||||||
|
- ✅ 始终使用最新代码构建
|
||||||
|
- ✅ 开发者友好
|
||||||
|
|
||||||
|
**缺点:**
|
||||||
|
- ❌ 要求用户有Node.js环境
|
||||||
|
- ❌ 安装时间长(需要npm install + build)
|
||||||
|
- ❌ 可能构建失败影响安装体验
|
||||||
|
- ❌ ComfyUI Manager可能不支持
|
||||||
|
|
||||||
|
**实现方式:**
|
||||||
|
1. 保持 `.gitignore` 设置
|
||||||
|
2. 添加安装脚本自动检测并构建
|
||||||
|
3. 在Python `__init__.py` 启动时检查构建产物
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 开发环境
|
||||||
|
- 技术用户
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案3:混合方案(开发 + 生产分离)
|
||||||
|
|
||||||
|
**优点:**
|
||||||
|
- ✅ 开发时只提交源代码
|
||||||
|
- ✅ Release时提供完整构建
|
||||||
|
- ✅ Git仓库保持干净
|
||||||
|
- ✅ 用户安装release版本即可用
|
||||||
|
|
||||||
|
**缺点:**
|
||||||
|
- ⚠️ 需要CI/CD配置
|
||||||
|
- ⚠️ 工作流稍复杂
|
||||||
|
|
||||||
|
**实现方式:**
|
||||||
|
1. 开发分支:gitignore构建产物
|
||||||
|
2. GitHub Actions:自动构建
|
||||||
|
3. Release分支/Tag:包含构建产物
|
||||||
|
4. 用户安装release版本
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 成熟项目
|
||||||
|
- 多人协作开发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案4:Python启动时自动构建(智能方案)
|
||||||
|
|
||||||
|
**优点:**
|
||||||
|
- ✅ 自动检测是否需要构建
|
||||||
|
- ✅ 开发模式自动构建
|
||||||
|
- ✅ 生产模式使用已有构建
|
||||||
|
- ✅ 最灵活
|
||||||
|
|
||||||
|
**缺点:**
|
||||||
|
- ⚠️ 需要编写构建检测逻辑
|
||||||
|
- ⚠️ 首次启动可能较慢
|
||||||
|
|
||||||
|
**实现方式:**
|
||||||
|
1. 在 `__init__.py` 中检查构建产物
|
||||||
|
2. 如果不存在或过期,尝试自动构建
|
||||||
|
3. 如果无Node.js环境,给出明确提示
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 开发+生产通用
|
||||||
|
- 技术用户为主
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推荐实现:方案1 + 方案4 组合
|
||||||
|
|
||||||
|
### 为什么?
|
||||||
|
|
||||||
|
1. **对普通用户**:提交构建产物,安装即用
|
||||||
|
2. **对开发者**:pre-commit hook确保提交前构建
|
||||||
|
3. **智能检测**:Python启动时检查,开发模式可自动重建
|
||||||
|
|
||||||
|
### 实现步骤
|
||||||
|
|
||||||
|
#### Step 1: 修改 .gitignore(提交构建产物)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 移除这行:
|
||||||
|
# web/comfyui/vue-widgets/
|
||||||
|
|
||||||
|
# 但保留源码构建缓存:
|
||||||
|
vue-widgets/node_modules/
|
||||||
|
vue-widgets/dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: 添加 pre-commit hook
|
||||||
|
|
||||||
|
创建 `.husky/pre-commit` 或使用简单的git hook:
|
||||||
|
```bash
|
||||||
|
#!/bin/sh
|
||||||
|
# 在commit前自动构建Vue widgets
|
||||||
|
cd vue-widgets && npm run build && cd .. && git add web/comfyui/vue-widgets/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: 在Python中添加智能检测
|
||||||
|
|
||||||
|
在 `__init__.py` 或专门的构建检查模块中:
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def check_vue_widgets_build():
|
||||||
|
"""检查Vue widgets是否已构建,如果需要则自动构建"""
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
build_file = project_root / "web/comfyui/vue-widgets/demo-widget.js"
|
||||||
|
src_dir = project_root / "vue-widgets/src"
|
||||||
|
|
||||||
|
# 如果构建产物不存在
|
||||||
|
if not build_file.exists():
|
||||||
|
print("[LoRA Manager] Vue widget build not found, attempting to build...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["npm", "run", "build"],
|
||||||
|
cwd=project_root / "vue-widgets",
|
||||||
|
capture_output=True,
|
||||||
|
timeout=120
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("[LoRA Manager] ✓ Vue widgets built successfully")
|
||||||
|
else:
|
||||||
|
print(f"[LoRA Manager] ⚠️ Build failed: {result.stderr.decode()}")
|
||||||
|
print("[LoRA Manager] Please run: cd vue-widgets && npm install && npm run build")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("[LoRA Manager] ⚠️ Node.js not found. Please install Node.js and run:")
|
||||||
|
print("[LoRA Manager] cd vue-widgets && npm install && npm run build")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[LoRA Manager] ⚠️ Build error: {e}")
|
||||||
|
|
||||||
|
# 检查源代码是否比构建产物新(开发模式)
|
||||||
|
elif src_dir.exists():
|
||||||
|
src_mtime = max(f.stat().st_mtime for f in src_dir.rglob("*") if f.is_file())
|
||||||
|
build_mtime = build_file.stat().st_mtime
|
||||||
|
|
||||||
|
if src_mtime > build_mtime:
|
||||||
|
print("[LoRA Manager] Source code newer than build, rebuilding...")
|
||||||
|
# 同样的构建逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: package.json 添加便捷脚本
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:check": "node scripts/check-build.js",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 对于不同场景的建议
|
||||||
|
|
||||||
|
### 场景A:当前开发阶段(快速验证)
|
||||||
|
**使用:方案1(提交构建产物)**
|
||||||
|
```bash
|
||||||
|
# 1. 移除gitignore
|
||||||
|
# 2. 构建并提交
|
||||||
|
cd vue-widgets && npm run build && cd ..
|
||||||
|
git add -f web/comfyui/vue-widgets/
|
||||||
|
git commit -m "Add Vue widget build output"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景B:多人协作开发
|
||||||
|
**使用:方案1 + pre-commit hook**
|
||||||
|
- 提交构建产物保证可用性
|
||||||
|
- Hook确保开发者不会忘记构建
|
||||||
|
|
||||||
|
### 场景C:成熟生产项目
|
||||||
|
**使用:方案3(GitHub Actions)**
|
||||||
|
- main分支不含构建产物
|
||||||
|
- CI自动构建并发布到release
|
||||||
|
- 用户安装release tag
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 立即可用的解决方案
|
||||||
|
|
||||||
|
### 最简单方案(推荐现在使用):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 修改 .gitignore,移除构建产物忽略
|
||||||
|
sed -i '/web\/comfyui\/vue-widgets/d' .gitignore
|
||||||
|
|
||||||
|
# 2. 添加源码缓存到gitignore
|
||||||
|
echo "vue-widgets/node_modules/" >> .gitignore
|
||||||
|
echo "vue-widgets/.vite/" >> .gitignore
|
||||||
|
|
||||||
|
# 3. 确保已构建
|
||||||
|
cd vue-widgets && npm run build && cd ..
|
||||||
|
|
||||||
|
# 4. 提交所有文件
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: Add Vue + PrimeVue widget scaffold with demo"
|
||||||
|
```
|
||||||
|
|
||||||
|
这样用户clone后即可直接使用,同时开发者在修改Vue代码后需要手动运行 `npm run build`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未来改进
|
||||||
|
|
||||||
|
可以考虑:
|
||||||
|
1. 添加 Husky + lint-staged 自动化pre-commit
|
||||||
|
2. 添加 GitHub Actions 自动构建和发布
|
||||||
|
3. 编写安装后检查脚本
|
||||||
|
4. 在ComfyUI Manager元数据中说明Node.js依赖(如果选择方案2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
| 方案 | 用户体验 | 开发体验 | Git仓库大小 | 实现难度 |
|
||||||
|
|------|---------|---------|------------|---------|
|
||||||
|
| 方案1 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
| 方案2 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| 方案3 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||||
|
| 方案4 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
|
||||||
|
|
||||||
|
**当前阶段推荐:方案1(提交构建产物)**
|
||||||
|
**长期推荐:方案1 + 方案4 组合(提交产物 + 智能检测)**
|
||||||
211
CLAUDE.md
Normal file
211
CLAUDE.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ComfyUI LoRA Manager is a comprehensive LoRA management system for ComfyUI that combines a Python backend with browser-based widgets. It provides model organization, downloading from CivitAI/CivArchive, recipe management, and one-click workflow integration.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Backend Development
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Install development dependencies (for testing)
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
|
||||||
|
# Run standalone server (port 8188 by default)
|
||||||
|
python standalone.py --port 8188
|
||||||
|
|
||||||
|
# Run backend tests with coverage
|
||||||
|
COVERAGE_FILE=coverage/backend/.coverage pytest \
|
||||||
|
--cov=py \
|
||||||
|
--cov=standalone \
|
||||||
|
--cov-report=term-missing \
|
||||||
|
--cov-report=html:coverage/backend/html \
|
||||||
|
--cov-report=xml:coverage/backend/coverage.xml \
|
||||||
|
--cov-report=json:coverage/backend/coverage.json
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/test_recipes.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Development
|
||||||
|
```bash
|
||||||
|
# Install frontend dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run frontend tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run frontend tests in watch mode
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run frontend tests with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
```bash
|
||||||
|
# Sync translation keys after UI string updates
|
||||||
|
python scripts/sync_translation_keys.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Backend Structure (Python)
|
||||||
|
|
||||||
|
**Core Entry Points:**
|
||||||
|
- `__init__.py` - ComfyUI plugin entry point, registers nodes and routes
|
||||||
|
- `standalone.py` - Standalone server that mocks ComfyUI dependencies
|
||||||
|
- `py/lora_manager.py` - Main LoraManager class that registers HTTP routes
|
||||||
|
|
||||||
|
**Service Layer** (`py/services/`):
|
||||||
|
- `ServiceRegistry` - Singleton service registry for dependency management
|
||||||
|
- `ModelServiceFactory` - Factory for creating model services (LoRA, Checkpoint, Embedding)
|
||||||
|
- Scanner services (`lora_scanner.py`, `checkpoint_scanner.py`, `embedding_scanner.py`) - Model file discovery and indexing
|
||||||
|
- `model_scanner.py` - Base scanner with hash-based deduplication and metadata extraction
|
||||||
|
- `persistent_model_cache.py` - SQLite-based cache for model metadata
|
||||||
|
- `metadata_sync_service.py` - Syncs metadata from CivitAI/CivArchive APIs
|
||||||
|
- `civitai_client.py` / `civarchive_client.py` - API clients for external services
|
||||||
|
- `downloader.py` / `download_manager.py` - Model download orchestration
|
||||||
|
- `recipe_scanner.py` - Recipe file management and image association
|
||||||
|
- `settings_manager.py` - Application settings with migration support
|
||||||
|
- `websocket_manager.py` - WebSocket broadcasting for real-time updates
|
||||||
|
- `use_cases/` - Business logic orchestration (auto-organize, bulk refresh, downloads)
|
||||||
|
|
||||||
|
**Routes Layer** (`py/routes/`):
|
||||||
|
- Route registrars organize endpoints by domain (models, recipes, previews, example images, updates)
|
||||||
|
- `handlers/` - Request handlers implementing business logic
|
||||||
|
- Routes use aiohttp and integrate with ComfyUI's PromptServer
|
||||||
|
|
||||||
|
**Recipe System** (`py/recipes/`):
|
||||||
|
- `base.py` - Base recipe metadata structure
|
||||||
|
- `enrichment.py` - Enriches recipes with model metadata
|
||||||
|
- `merger.py` - Merges recipe data from multiple sources
|
||||||
|
- `parsers/` - Parsers for different recipe formats (PNG, JSON, workflow)
|
||||||
|
|
||||||
|
**Custom Nodes** (`py/nodes/`):
|
||||||
|
- `lora_loader.py` - LoRA loader nodes with preset support
|
||||||
|
- `save_image.py` - Enhanced save image with pattern-based filenames
|
||||||
|
- `trigger_word_toggle.py` - Toggle trigger words in prompts
|
||||||
|
- `lora_stacker.py` - Stack multiple LoRAs
|
||||||
|
- `prompt.py` - Prompt node with autocomplete
|
||||||
|
- `wanvideo_lora_select.py` - WanVideo-specific LoRA selection
|
||||||
|
|
||||||
|
**Configuration** (`py/config.py`):
|
||||||
|
- Manages folder paths for models, checkpoints, embeddings
|
||||||
|
- Handles symlink mappings for complex directory structures
|
||||||
|
- Auto-saves paths to settings.json in ComfyUI mode
|
||||||
|
|
||||||
|
### Frontend Structure (JavaScript)
|
||||||
|
|
||||||
|
**ComfyUI Widgets** (`web/comfyui/`):
|
||||||
|
- Vanilla JavaScript ES modules extending ComfyUI's LiteGraph-based UI
|
||||||
|
- `loras_widget.js` - Main LoRA selection widget with preview
|
||||||
|
- `loras_widget_events.js` - Event handling for widget interactions
|
||||||
|
- `autocomplete.js` - Autocomplete for trigger words and embeddings
|
||||||
|
- `preview_tooltip.js` - Preview tooltip for model cards
|
||||||
|
- `top_menu_extension.js` - Adds "Launch LoRA Manager" menu item
|
||||||
|
- `trigger_word_highlight.js` - Syntax highlighting for trigger words
|
||||||
|
- `utils.js` - Shared utilities and API helpers
|
||||||
|
|
||||||
|
**Widget Development:**
|
||||||
|
- Widgets use `app.registerExtension` and `getCustomWidgets` hooks
|
||||||
|
- `node.addDOMWidget(name, type, element, options)` embeds HTML in nodes
|
||||||
|
- See `docs/dom_widget_dev_guide.md` for complete DOMWidget development guide
|
||||||
|
|
||||||
|
**Web Source** (`web-src/`):
|
||||||
|
- Modern frontend components (if migrating from static)
|
||||||
|
- `components/` - Reusable UI components
|
||||||
|
- `styles/` - CSS styling
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
**Dual Mode Operation:**
|
||||||
|
- ComfyUI plugin mode: Integrates with ComfyUI's PromptServer, uses folder_paths
|
||||||
|
- Standalone mode: Mocks ComfyUI dependencies via `standalone.py`, reads paths from settings.json
|
||||||
|
- Detection: `os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"`
|
||||||
|
|
||||||
|
**Settings Management:**
|
||||||
|
- Settings stored in user directory (via `platformdirs`) or portable mode (in repo)
|
||||||
|
- Migration system tracks settings schema version
|
||||||
|
- Template in `settings.json.example` defines defaults
|
||||||
|
|
||||||
|
**Model Scanning Flow:**
|
||||||
|
1. Scanner walks folder paths, computes file hashes
|
||||||
|
2. Hash-based deduplication prevents duplicate processing
|
||||||
|
3. Metadata extracted from safetensors headers
|
||||||
|
4. Persistent cache stores results in SQLite
|
||||||
|
5. Background sync fetches CivitAI/CivArchive metadata
|
||||||
|
6. WebSocket broadcasts updates to connected clients
|
||||||
|
|
||||||
|
**Recipe System:**
|
||||||
|
- Recipes store LoRA combinations with parameters
|
||||||
|
- Supports import from workflow JSON, PNG metadata
|
||||||
|
- Images associated with recipes via sibling file detection
|
||||||
|
- Enrichment adds model metadata for display
|
||||||
|
|
||||||
|
**Frontend-Backend Communication:**
|
||||||
|
- REST API for CRUD operations
|
||||||
|
- WebSocket for real-time progress updates (downloads, scans)
|
||||||
|
- API endpoints follow `/loras/*` pattern
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
- PEP 8 with 4-space indentation
|
||||||
|
- snake_case for files, functions, variables
|
||||||
|
- PascalCase for classes
|
||||||
|
- Type hints preferred
|
||||||
|
- English comments only (per copilot-instructions.md)
|
||||||
|
- Loggers via `logging.getLogger(__name__)`
|
||||||
|
|
||||||
|
**JavaScript:**
|
||||||
|
- ES modules with camelCase
|
||||||
|
- Files use `*_widget.js` suffix for ComfyUI widgets
|
||||||
|
- Prefer vanilla JS, avoid framework dependencies
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**Backend Tests:**
|
||||||
|
- pytest with `--import-mode=importlib`
|
||||||
|
- Test files: `tests/test_*.py`
|
||||||
|
- Fixtures in `tests/conftest.py`
|
||||||
|
- Mock ComfyUI dependencies using standalone.py patterns
|
||||||
|
- Markers: `@pytest.mark.asyncio` for async tests, `@pytest.mark.no_settings_dir_isolation` for real paths
|
||||||
|
|
||||||
|
**Frontend Tests:**
|
||||||
|
- Vitest with jsdom environment
|
||||||
|
- Test files: `tests/frontend/**/*.test.js`
|
||||||
|
- Setup in `tests/frontend/setup.js`
|
||||||
|
- Coverage via `npm run test:coverage`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
**Settings Location:**
|
||||||
|
- ComfyUI mode: Auto-saves folder paths to user settings directory
|
||||||
|
- Standalone mode: Use `settings.json` (copy from `settings.json.example`)
|
||||||
|
- Portable mode: Set `"use_portable_settings": true` in settings.json
|
||||||
|
|
||||||
|
**API Integration:**
|
||||||
|
- CivitAI API key required for downloads (add to settings)
|
||||||
|
- CivArchive API used as fallback for deleted models
|
||||||
|
- Metadata archive database available for offline metadata
|
||||||
|
|
||||||
|
**Symlink Handling:**
|
||||||
|
- Config scans symlinks to map virtual paths to physical locations
|
||||||
|
- Preview validation uses normalized preview root paths
|
||||||
|
- Fingerprinting prevents redundant symlink rescans
|
||||||
|
|
||||||
|
**ComfyUI Node Development:**
|
||||||
|
- Nodes defined in `py/nodes/`, registered in `__init__.py`
|
||||||
|
- Frontend widgets in `web/comfyui/`, matched by node type
|
||||||
|
- Use `WEB_DIRECTORY = "./web/comfyui"` convention
|
||||||
|
|
||||||
|
**Recipe Image Association:**
|
||||||
|
- Recipes scan for sibling images in same directory
|
||||||
|
- Supports repair/migration of recipe image paths
|
||||||
|
- See `py/services/recipe_scanner.py` for implementation details
|
||||||
290
QUICK_REFERENCE.md
Normal file
290
QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# Vue Widget 开发快速参考
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 用户安装
|
||||||
|
```bash
|
||||||
|
# Clone仓库后直接启动ComfyUI,无需其他步骤
|
||||||
|
git clone <repo>
|
||||||
|
# 构建产物已包含,直接可用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发者设置
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm install # 首次安装依赖
|
||||||
|
npm run build # 构建widget
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 常用命令
|
||||||
|
|
||||||
|
### 构建命令
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
|
||||||
|
npm run build # 单次构建
|
||||||
|
npm run dev # 监听模式(自动重建)
|
||||||
|
npm run rebuild # 清理并重建
|
||||||
|
npm run typecheck # TypeScript类型检查
|
||||||
|
npm run clean # 清理构建产物
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发工作流
|
||||||
|
```bash
|
||||||
|
# 方式1: 监听模式(推荐)
|
||||||
|
cd vue-widgets && npm run dev
|
||||||
|
# 修改代码后,刷新ComfyUI浏览器页面
|
||||||
|
|
||||||
|
# 方式2: 手动构建
|
||||||
|
# 修改代码 → npm run build → 刷新浏览器
|
||||||
|
|
||||||
|
# 方式3: 自动检测
|
||||||
|
# 修改代码 → 重启ComfyUI(会自动重建)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
vue-widgets/ # Vue源代码目录
|
||||||
|
├── src/
|
||||||
|
│ ├── main.ts # 扩展注册入口
|
||||||
|
│ └── components/
|
||||||
|
│ └── DemoWidget.vue # Widget组件
|
||||||
|
├── package.json # 依赖和脚本
|
||||||
|
└── vite.config.mts # 构建配置
|
||||||
|
|
||||||
|
web/comfyui/vue-widgets/ # 构建产物(提交到Git)
|
||||||
|
├── demo-widget.js # 编译后的JS
|
||||||
|
├── demo-widget.js.map # Source map
|
||||||
|
└── assets/ # CSS等资源
|
||||||
|
|
||||||
|
py/nodes/
|
||||||
|
└── demo_vue_widget_node.py # Python节点定义
|
||||||
|
|
||||||
|
py/
|
||||||
|
└── vue_widget_builder.py # 构建检测模块
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 创建新Widget
|
||||||
|
|
||||||
|
### 1. 创建Python节点
|
||||||
|
`py/nodes/my_widget_node.py`:
|
||||||
|
```python
|
||||||
|
class MyWidgetNode:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"my_widget": ("MY_WIDGET", {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
FUNCTION = "process"
|
||||||
|
CATEGORY = "loramanager"
|
||||||
|
|
||||||
|
def process(self, my_widget):
|
||||||
|
return (str(my_widget),)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {"MyWidgetNode": MyWidgetNode}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 创建Vue组件
|
||||||
|
`vue-widgets/src/components/MyWidget.vue`:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Button label="Click" @click="handleClick" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
widget: { serializeValue?: Function }
|
||||||
|
node: { id: number }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.widget.serializeValue = async () => {
|
||||||
|
return { /* 数据 */ }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 注册Widget
|
||||||
|
`vue-widgets/src/main.ts`:
|
||||||
|
```typescript
|
||||||
|
import MyWidget from '@/components/MyWidget.vue'
|
||||||
|
|
||||||
|
// 在 getCustomWidgets() 中:
|
||||||
|
MY_WIDGET(node) {
|
||||||
|
return createVueWidget(node, MyWidget, 'my-widget')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 注册节点
|
||||||
|
`__init__.py`:
|
||||||
|
```python
|
||||||
|
from .py.nodes.my_widget_node import MyWidgetNode
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
# ...
|
||||||
|
"MyWidgetNode": MyWidgetNode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 构建并测试
|
||||||
|
```bash
|
||||||
|
cd vue-widgets && npm run build
|
||||||
|
# 重启ComfyUI并测试
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 构建流程保证
|
||||||
|
|
||||||
|
### 如何确保用户安装后有最新的构建产物?
|
||||||
|
|
||||||
|
**实施的方案**: 提交构建产物 + 智能检测
|
||||||
|
|
||||||
|
#### ✅ 对用户
|
||||||
|
1. 构建产物已包含在Git仓库中
|
||||||
|
2. Clone后即可使用,无需Node.js
|
||||||
|
3. ComfyUI启动时自动检查(如果有Node.js会自动重建)
|
||||||
|
|
||||||
|
#### ✅ 对开发者
|
||||||
|
1. 修改Vue代码后,ComfyUI重启时自动检测并重建
|
||||||
|
2. 可使用 `npm run dev` 监听模式自动重建
|
||||||
|
3. 提交前运行 `npm run build`(或使用pre-commit hook)
|
||||||
|
|
||||||
|
#### ✅ Git配置
|
||||||
|
```bash
|
||||||
|
# .gitignore
|
||||||
|
✅ 提交: web/comfyui/vue-widgets/ # 构建产物
|
||||||
|
❌ 忽略: vue-widgets/node_modules/ # 开发依赖
|
||||||
|
❌ 忽略: vue-widgets/.vite/ # 构建缓存
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 故障排除
|
||||||
|
|
||||||
|
### 问题: Widget不显示最新修改
|
||||||
|
```bash
|
||||||
|
# 解决方案1: 强制重建
|
||||||
|
cd vue-widgets && npm run rebuild
|
||||||
|
|
||||||
|
# 解决方案2: 清理缓存
|
||||||
|
rm -rf web/comfyui/vue-widgets
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 解决方案3: 硬刷新浏览器
|
||||||
|
Ctrl+Shift+R (Windows/Linux)
|
||||||
|
Cmd+Shift+R (Mac)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题: 自动构建失败
|
||||||
|
```bash
|
||||||
|
# 检查Node.js
|
||||||
|
node --version # 需要 >= 18
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
# 重新安装依赖
|
||||||
|
cd vue-widgets
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题: TypeScript错误
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm run typecheck # 检查类型错误
|
||||||
|
npm run build # 构建(忽略类型错误)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 常用PrimeVue组件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import InputNumber from 'primevue/inputnumber'
|
||||||
|
import Dropdown from 'primevue/dropdown'
|
||||||
|
import Card from 'primevue/card'
|
||||||
|
import DataTable from 'primevue/datatable'
|
||||||
|
import Column from 'primevue/column'
|
||||||
|
import Dialog from 'primevue/dialog'
|
||||||
|
import Tree from 'primevue/tree'
|
||||||
|
import Checkbox from 'primevue/checkbox'
|
||||||
|
import Slider from 'primevue/slider'
|
||||||
|
```
|
||||||
|
|
||||||
|
文档: https://primevue.org/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Git工作流
|
||||||
|
|
||||||
|
### 提交代码
|
||||||
|
```bash
|
||||||
|
# 方式1: 手动构建
|
||||||
|
cd vue-widgets && npm run build && cd ..
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: update widget"
|
||||||
|
|
||||||
|
# 方式2: 使用pre-commit hook
|
||||||
|
cp vue-widgets/pre-commit.example .git/hooks/pre-commit
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
git commit -m "feat: update widget"
|
||||||
|
# Hook自动构建并添加产物
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提交前检查清单
|
||||||
|
- [ ] Vue源代码已修改
|
||||||
|
- [ ] 运行 `npm run build`
|
||||||
|
- [ ] 测试widget功能正常
|
||||||
|
- [ ] 构建产物已生成在 `web/comfyui/vue-widgets/`
|
||||||
|
- [ ] `git add` 包含构建产物
|
||||||
|
- [ ] Commit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 更多文档
|
||||||
|
|
||||||
|
- **VUE_WIDGETS_SETUP.md** - 完整架构和设置指南
|
||||||
|
- **vue-widgets/README.md** - Widget开发详细指南
|
||||||
|
- **vue-widgets/DEMO_INSTRUCTIONS.md** - Demo widget测试说明
|
||||||
|
- **BUILD_WORKFLOW_SOLUTIONS.md** - 构建流程方案对比
|
||||||
|
- **BUILD_WORKFLOW_IMPLEMENTATION.md** - 已实施方案详解
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 提示
|
||||||
|
|
||||||
|
- 开发时优先使用 `npm run dev` 监听模式
|
||||||
|
- 提交前确保运行 `npm run build`
|
||||||
|
- 构建产物约1.4MB,会提交到Git(正常)
|
||||||
|
- ComfyUI会在启动时自动检测并重建(如果需要)
|
||||||
|
- Source maps已启用,便于调试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 学习资源
|
||||||
|
|
||||||
|
- [Vue 3 文档](https://vuejs.org/)
|
||||||
|
- [PrimeVue 文档](https://primevue.org/)
|
||||||
|
- [TypeScript 文档](https://www.typescriptlang.org/)
|
||||||
|
- [Vite 文档](https://vitejs.dev/)
|
||||||
|
- [ComfyUI 自定义节点开发](https://docs.comfy.org/)
|
||||||
181
VUE_WIDGETS_SETUP.md
Normal file
181
VUE_WIDGETS_SETUP.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Vue + PrimeVue Widget Development Setup
|
||||||
|
|
||||||
|
This guide explains the Vue + PrimeVue widget development scaffold for ComfyUI LoRA Manager.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The project now supports developing custom ComfyUI widgets using Vue 3 + PrimeVue, providing a modern reactive framework for building rich UI components.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
ComfyUI-Lora-Manager/
|
||||||
|
├── vue-widgets/ # Vue widget source code (TypeScript)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main.ts # Extension registration
|
||||||
|
│ │ └── components/ # Vue components
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── vite.config.mts # Build to web/comfyui/vue-widgets/
|
||||||
|
│ └── tsconfig.json
|
||||||
|
│
|
||||||
|
├── web/comfyui/ # ComfyUI web directory
|
||||||
|
│ ├── vue-widgets/ # Compiled Vue widgets (gitignored)
|
||||||
|
│ │ ├── demo-widget.js # Built JavaScript
|
||||||
|
│ │ └── assets/ # CSS and other assets
|
||||||
|
│ └── *.js # Existing vanilla JS widgets
|
||||||
|
│
|
||||||
|
├── py/nodes/ # Python node definitions
|
||||||
|
│ ├── demo_vue_widget_node.py # Demo node
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── __init__.py # Node registration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build the Demo Widget
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This compiles the TypeScript/Vue code and outputs to `web/comfyui/vue-widgets/`.
|
||||||
|
|
||||||
|
### 3. Test in ComfyUI
|
||||||
|
|
||||||
|
1. Start/restart ComfyUI
|
||||||
|
2. Open the ComfyUI interface
|
||||||
|
3. Add the "LoRA Manager Demo (Vue)" node from the node menu
|
||||||
|
4. You should see a Vue-powered widget with PrimeVue components:
|
||||||
|
- Text input for model name
|
||||||
|
- Number input for strength (with +/- buttons)
|
||||||
|
- Apply and Reset buttons
|
||||||
|
- Result card showing current configuration
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Watch Mode for Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This watches for file changes and automatically rebuilds. You'll need to refresh ComfyUI's browser page to see changes.
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
**Python Side (`py/nodes/demo_vue_widget_node.py`):**
|
||||||
|
- Defines the ComfyUI node class
|
||||||
|
- Specifies input types (including the custom widget type)
|
||||||
|
- Implements the processing logic
|
||||||
|
- The widget type name must match the key in the frontend's `getCustomWidgets()`
|
||||||
|
|
||||||
|
**Frontend Side (`vue-widgets/src/`):**
|
||||||
|
- `main.ts` - Registers the extension with ComfyUI and creates Vue apps
|
||||||
|
- `components/DemoWidget.vue` - The actual Vue component with PrimeVue UI
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **Widget Creation:**
|
||||||
|
- ComfyUI calls `getCustomWidgets()` when creating a node
|
||||||
|
- Creates a container DOM element
|
||||||
|
- Mounts a Vue app with the component inside the container
|
||||||
|
|
||||||
|
2. **Widget State:**
|
||||||
|
- Component props receive `widget` and `node` objects from ComfyUI
|
||||||
|
- Use Vue's reactive state management within the component
|
||||||
|
|
||||||
|
3. **Serialization:**
|
||||||
|
- Implement `widget.serializeValue()` in `onMounted()`
|
||||||
|
- This function is called when the workflow is saved or executed
|
||||||
|
- Return the data that should be passed to the Python node
|
||||||
|
|
||||||
|
4. **Processing:**
|
||||||
|
- Python node receives the serialized data in its `process()` method
|
||||||
|
- Process the data and return results to the workflow
|
||||||
|
|
||||||
|
## Creating Your Own Widget
|
||||||
|
|
||||||
|
See the detailed guide in `vue-widgets/README.md`.
|
||||||
|
|
||||||
|
Quick checklist:
|
||||||
|
- [ ] Create Python node in `py/nodes/`
|
||||||
|
- [ ] Create Vue component in `vue-widgets/src/components/`
|
||||||
|
- [ ] Register widget in `vue-widgets/src/main.ts`
|
||||||
|
- [ ] Register node in `__init__.py`
|
||||||
|
- [ ] Build with `npm run build`
|
||||||
|
- [ ] Test in ComfyUI
|
||||||
|
|
||||||
|
## Key Technologies
|
||||||
|
|
||||||
|
- **Vue 3**: Modern reactive framework with Composition API
|
||||||
|
- **PrimeVue 4**: Rich UI component library with 90+ components
|
||||||
|
- **TypeScript**: Type-safe development
|
||||||
|
- **Vite**: Fast build tool with HMR support
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
The build is configured to:
|
||||||
|
- Output ES modules to `../web/comfyui/vue-widgets/`
|
||||||
|
- Mark ComfyUI's `app.js` as external (not bundled)
|
||||||
|
- Generate source maps for debugging
|
||||||
|
- Keep code unminified for easier debugging
|
||||||
|
- Split vendor code into separate chunks
|
||||||
|
|
||||||
|
## PrimeVue Components
|
||||||
|
|
||||||
|
The demo widget showcases several PrimeVue components:
|
||||||
|
- `Button` - Styled buttons with icons
|
||||||
|
- `InputText` - Text input fields
|
||||||
|
- `InputNumber` - Number inputs with spinners
|
||||||
|
- `Card` - Container component
|
||||||
|
|
||||||
|
For the full component library, see [PrimeVue Documentation](https://primevue.org/).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build fails with module errors
|
||||||
|
- Make sure you're in the `vue-widgets` directory
|
||||||
|
- Run `npm install` to ensure all dependencies are installed
|
||||||
|
- Check that Node.js version is 18+ (`node --version`)
|
||||||
|
|
||||||
|
### Widget doesn't appear in ComfyUI
|
||||||
|
- Verify the build completed successfully (`web/comfyui/vue-widgets/demo-widget.js` exists)
|
||||||
|
- Check that the Python node is registered in `__init__.py`
|
||||||
|
- Restart ComfyUI completely (not just refresh browser)
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
|
||||||
|
### Widget type mismatch error
|
||||||
|
- Ensure the widget type in Python (e.g., `"LORA_DEMO_WIDGET"`) matches the key in `getCustomWidgets()`
|
||||||
|
- Type names are case-sensitive
|
||||||
|
|
||||||
|
### Changes not reflected after rebuild
|
||||||
|
- Hard refresh the browser (Ctrl+Shift+R / Cmd+Shift+R)
|
||||||
|
- Clear browser cache
|
||||||
|
- Restart ComfyUI server
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that the scaffold is set up, you can:
|
||||||
|
|
||||||
|
1. **Extend the demo widget** - Add more PrimeVue components and functionality
|
||||||
|
2. **Create production widgets** - Build widgets for actual LoRA management features
|
||||||
|
3. **Add styling** - Customize the look with CSS/Tailwind
|
||||||
|
4. **Add i18n** - Implement vue-i18n for internationalization
|
||||||
|
5. **Add state management** - Use Pinia if you need shared state across widgets
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ComfyUI Custom Nodes Documentation](https://docs.comfy.org/essentials/custom_node_server)
|
||||||
|
- [PrimeVue Documentation](https://primevue.org/)
|
||||||
|
- [Vue 3 Documentation](https://vuejs.org/)
|
||||||
|
- [Vite Documentation](https://vitejs.dev/)
|
||||||
|
- Reference implementation: `/refs/ComfyUI_frontend_vue_basic`
|
||||||
19
__init__.py
19
__init__.py
@@ -8,6 +8,7 @@ try: # pragma: no cover - import fallback for pytest collection
|
|||||||
from .py.nodes.debug_metadata import DebugMetadata
|
from .py.nodes.debug_metadata import DebugMetadata
|
||||||
from .py.nodes.wanvideo_lora_select import WanVideoLoraSelectLM
|
from .py.nodes.wanvideo_lora_select import WanVideoLoraSelectLM
|
||||||
from .py.nodes.wanvideo_lora_select_from_text import WanVideoLoraSelectFromText
|
from .py.nodes.wanvideo_lora_select_from_text import WanVideoLoraSelectFromText
|
||||||
|
from .py.nodes.demo_vue_widget_node import LoraManagerDemoNode
|
||||||
from .py.metadata_collector import init as init_metadata_collector
|
from .py.metadata_collector import init as init_metadata_collector
|
||||||
except ImportError: # pragma: no cover - allows running under pytest without package install
|
except ImportError: # pragma: no cover - allows running under pytest without package install
|
||||||
import importlib
|
import importlib
|
||||||
@@ -28,6 +29,7 @@ except ImportError: # pragma: no cover - allows running under pytest without pa
|
|||||||
DebugMetadata = importlib.import_module("py.nodes.debug_metadata").DebugMetadata
|
DebugMetadata = importlib.import_module("py.nodes.debug_metadata").DebugMetadata
|
||||||
WanVideoLoraSelectLM = importlib.import_module("py.nodes.wanvideo_lora_select").WanVideoLoraSelectLM
|
WanVideoLoraSelectLM = importlib.import_module("py.nodes.wanvideo_lora_select").WanVideoLoraSelectLM
|
||||||
WanVideoLoraSelectFromText = importlib.import_module("py.nodes.wanvideo_lora_select_from_text").WanVideoLoraSelectFromText
|
WanVideoLoraSelectFromText = importlib.import_module("py.nodes.wanvideo_lora_select_from_text").WanVideoLoraSelectFromText
|
||||||
|
LoraManagerDemoNode = importlib.import_module("py.nodes.demo_vue_widget_node").LoraManagerDemoNode
|
||||||
init_metadata_collector = importlib.import_module("py.metadata_collector").init
|
init_metadata_collector = importlib.import_module("py.metadata_collector").init
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
@@ -39,11 +41,26 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
SaveImageLM.NAME: SaveImageLM,
|
SaveImageLM.NAME: SaveImageLM,
|
||||||
DebugMetadata.NAME: DebugMetadata,
|
DebugMetadata.NAME: DebugMetadata,
|
||||||
WanVideoLoraSelectLM.NAME: WanVideoLoraSelectLM,
|
WanVideoLoraSelectLM.NAME: WanVideoLoraSelectLM,
|
||||||
WanVideoLoraSelectFromText.NAME: WanVideoLoraSelectFromText
|
WanVideoLoraSelectFromText.NAME: WanVideoLoraSelectFromText,
|
||||||
|
"LoraManagerDemoNode": LoraManagerDemoNode
|
||||||
}
|
}
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web/comfyui"
|
WEB_DIRECTORY = "./web/comfyui"
|
||||||
|
|
||||||
|
# Check and build Vue widgets if needed (development mode)
|
||||||
|
try:
|
||||||
|
from .py.vue_widget_builder import check_and_build_vue_widgets
|
||||||
|
# Auto-build in development, warn only if fails
|
||||||
|
check_and_build_vue_widgets(auto_build=True, warn_only=True)
|
||||||
|
except ImportError:
|
||||||
|
# Fallback for pytest
|
||||||
|
import importlib
|
||||||
|
check_and_build_vue_widgets = importlib.import_module("py.vue_widget_builder").check_and_build_vue_widgets
|
||||||
|
check_and_build_vue_widgets(auto_build=True, warn_only=True)
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logging.warning(f"[LoRA Manager] Vue widget build check skipped: {e}")
|
||||||
|
|
||||||
# Initialize metadata collector
|
# Initialize metadata collector
|
||||||
init_metadata_collector()
|
init_metadata_collector()
|
||||||
|
|
||||||
|
|||||||
72
py/nodes/demo_vue_widget_node.py
Normal file
72
py/nodes/demo_vue_widget_node.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Demo node to showcase Vue + PrimeVue widget integration in ComfyUI LoRA Manager.
|
||||||
|
|
||||||
|
This node demonstrates:
|
||||||
|
- Vue 3 + PrimeVue custom widget
|
||||||
|
- Widget state serialization
|
||||||
|
- Integration with ComfyUI workflow
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class LoraManagerDemoNode:
|
||||||
|
"""
|
||||||
|
A demo node that uses a Vue + PrimeVue widget to configure LoRA parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"lora_demo_widget": ("LORA_DEMO_WIDGET", {}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"text": ("STRING", {
|
||||||
|
"default": "",
|
||||||
|
"multiline": True,
|
||||||
|
"placeholder": "Additional prompt text..."
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "FLOAT", "STRING")
|
||||||
|
RETURN_NAMES = ("model_name", "strength", "info")
|
||||||
|
|
||||||
|
FUNCTION = "process"
|
||||||
|
|
||||||
|
CATEGORY = "loramanager/demo"
|
||||||
|
|
||||||
|
def process(self, lora_demo_widget, text=""):
|
||||||
|
"""
|
||||||
|
Process the widget data and return the configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lora_demo_widget: Widget data containing model_name and strength
|
||||||
|
text: Optional text input
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (model_name, strength, info_string)
|
||||||
|
"""
|
||||||
|
model_name = lora_demo_widget.get("modelName", "")
|
||||||
|
strength = lora_demo_widget.get("strength", 1.0)
|
||||||
|
|
||||||
|
info = f"Vue Widget Demo - Model: {model_name}, Strength: {strength}"
|
||||||
|
if text:
|
||||||
|
info += f"\nAdditional text: {text}"
|
||||||
|
|
||||||
|
print(f"[LoraManagerDemoNode] {info}")
|
||||||
|
|
||||||
|
return (model_name, strength, info)
|
||||||
|
|
||||||
|
|
||||||
|
# Node class mappings for ComfyUI
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"LoraManagerDemoNode": LoraManagerDemoNode
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display name mappings
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"LoraManagerDemoNode": "LoRA Manager Demo (Vue)"
|
||||||
|
}
|
||||||
241
py/vue_widget_builder.py
Normal file
241
py/vue_widget_builder.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
"""
|
||||||
|
Vue Widget Build Checker and Auto-builder
|
||||||
|
|
||||||
|
This module checks if Vue widgets are built and attempts to build them if needed.
|
||||||
|
Useful for development mode where source code might be newer than build output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VueWidgetBuilder:
|
||||||
|
"""Manages Vue widget build checking and auto-building."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None):
|
||||||
|
"""
|
||||||
|
Initialize the builder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Project root directory. If None, auto-detects.
|
||||||
|
"""
|
||||||
|
if project_root is None:
|
||||||
|
# Auto-detect project root (where __init__.py is)
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
self.project_root = Path(project_root)
|
||||||
|
self.vue_widgets_dir = self.project_root / "vue-widgets"
|
||||||
|
self.build_output_dir = self.project_root / "web" / "comfyui" / "vue-widgets"
|
||||||
|
self.src_dir = self.vue_widgets_dir / "src"
|
||||||
|
|
||||||
|
def check_build_exists(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if build output exists.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if at least one built .js file exists
|
||||||
|
"""
|
||||||
|
if not self.build_output_dir.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
js_files = list(self.build_output_dir.glob("*.js"))
|
||||||
|
return len(js_files) > 0
|
||||||
|
|
||||||
|
def check_build_outdated(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if source code is newer than build output.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if source is newer, False otherwise or if can't determine
|
||||||
|
"""
|
||||||
|
if not self.src_dir.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.check_build_exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get newest file in source directory
|
||||||
|
src_files = [f for f in self.src_dir.rglob("*") if f.is_file()]
|
||||||
|
if not src_files:
|
||||||
|
return False
|
||||||
|
|
||||||
|
newest_src_time = max(f.stat().st_mtime for f in src_files)
|
||||||
|
|
||||||
|
# Get oldest file in build directory
|
||||||
|
build_files = [f for f in self.build_output_dir.rglob("*.js") if f.is_file()]
|
||||||
|
if not build_files:
|
||||||
|
return True
|
||||||
|
|
||||||
|
oldest_build_time = min(f.stat().st_mtime for f in build_files)
|
||||||
|
|
||||||
|
return newest_src_time > oldest_build_time
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking build timestamps: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_node_available(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if Node.js is available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if node/npm are available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["npm", "--version"],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=5,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def build_widgets(self, force: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Build Vue widgets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: If True, build even if not needed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if build succeeded or not needed, False if failed
|
||||||
|
"""
|
||||||
|
if not force and self.check_build_exists() and not self.check_build_outdated():
|
||||||
|
logger.debug("Vue widgets build is up to date")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.vue_widgets_dir.exists():
|
||||||
|
logger.warning(f"Vue widgets directory not found: {self.vue_widgets_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.check_node_available():
|
||||||
|
logger.warning(
|
||||||
|
"Node.js/npm not found. Cannot build Vue widgets. "
|
||||||
|
"Please install Node.js or build manually: cd vue-widgets && npm run build"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Building Vue widgets...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if node_modules exists, if not run npm install first
|
||||||
|
node_modules = self.vue_widgets_dir / "node_modules"
|
||||||
|
if not node_modules.exists():
|
||||||
|
logger.info("Installing npm dependencies...")
|
||||||
|
install_result = subprocess.run(
|
||||||
|
["npm", "install"],
|
||||||
|
cwd=self.vue_widgets_dir,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=300, # 5 minutes for install
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if install_result.returncode != 0:
|
||||||
|
logger.error(f"npm install failed: {install_result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Run build
|
||||||
|
build_result = subprocess.run(
|
||||||
|
["npm", "run", "build"],
|
||||||
|
cwd=self.vue_widgets_dir,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=120, # 2 minutes for build
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if build_result.returncode == 0:
|
||||||
|
logger.info("✓ Vue widgets built successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Build failed: {build_result.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("Build timed out")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Build error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ensure_built(self, auto_build: bool = True, warn_only: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
Ensure Vue widgets are built, optionally auto-building if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auto_build: If True, attempt to build if needed
|
||||||
|
warn_only: If True, only warn on failure instead of raising
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if widgets are available (built or successfully auto-built)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If warn_only=False and build is missing/failed
|
||||||
|
"""
|
||||||
|
if self.check_build_exists():
|
||||||
|
# Build exists, check if outdated
|
||||||
|
if self.check_build_outdated():
|
||||||
|
logger.info("Vue widget source code is newer than build")
|
||||||
|
if auto_build:
|
||||||
|
return self.build_widgets()
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Vue widget build is outdated. "
|
||||||
|
"Please rebuild: cd vue-widgets && npm run build"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# No build exists
|
||||||
|
logger.warning("Vue widget build not found")
|
||||||
|
|
||||||
|
if auto_build:
|
||||||
|
if self.build_widgets():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
msg = (
|
||||||
|
"Failed to build Vue widgets. "
|
||||||
|
"Please build manually: cd vue-widgets && npm install && npm run build"
|
||||||
|
)
|
||||||
|
if warn_only:
|
||||||
|
logger.warning(msg)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
else:
|
||||||
|
msg = "Vue widgets not built. Please run: cd vue-widgets && npm install && npm run build"
|
||||||
|
if warn_only:
|
||||||
|
logger.warning(msg)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def check_and_build_vue_widgets(
|
||||||
|
auto_build: bool = True,
|
||||||
|
warn_only: bool = True,
|
||||||
|
force: bool = False
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Convenience function to check and build Vue widgets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
auto_build: If True, attempt to build if needed
|
||||||
|
warn_only: If True, only warn on failure instead of raising
|
||||||
|
force: If True, force rebuild even if up to date
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if widgets are available
|
||||||
|
"""
|
||||||
|
builder = VueWidgetBuilder()
|
||||||
|
|
||||||
|
if force:
|
||||||
|
return builder.build_widgets(force=True)
|
||||||
|
|
||||||
|
return builder.ensure_built(auto_build=auto_build, warn_only=warn_only)
|
||||||
4
vue-widgets/.gitignore
vendored
Normal file
4
vue-widgets/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
.DS_Store
|
||||||
179
vue-widgets/DEMO_INSTRUCTIONS.md
Normal file
179
vue-widgets/DEMO_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Demo Widget Testing Instructions
|
||||||
|
|
||||||
|
## What Was Created
|
||||||
|
|
||||||
|
A complete Vue + PrimeVue development scaffold for creating custom ComfyUI widgets, including a working demo to validate the entire workflow.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. Python Node (`py/nodes/demo_vue_widget_node.py`)
|
||||||
|
- **Node Name**: LoRA Manager Demo (Vue)
|
||||||
|
- **Category**: loramanager/demo
|
||||||
|
- **Inputs**:
|
||||||
|
- `lora_demo_widget` (custom widget)
|
||||||
|
- `text` (optional string)
|
||||||
|
- **Outputs**:
|
||||||
|
- `model_name` (STRING)
|
||||||
|
- `strength` (FLOAT)
|
||||||
|
- `info` (STRING)
|
||||||
|
|
||||||
|
### 2. Vue Widget (`vue-widgets/src/components/DemoWidget.vue`)
|
||||||
|
Uses PrimeVue components:
|
||||||
|
- **InputText** - For model name input
|
||||||
|
- **InputNumber** - For strength value (0-2, step 0.1) with +/- buttons
|
||||||
|
- **Button** - Apply and Reset actions
|
||||||
|
- **Card** - Display current configuration
|
||||||
|
|
||||||
|
### 3. Build System
|
||||||
|
- **Vite** - Fast build tool
|
||||||
|
- **TypeScript** - Type-safe development
|
||||||
|
- **Output**: `web/comfyui/vue-widgets/demo-widget.js`
|
||||||
|
|
||||||
|
## How to Test
|
||||||
|
|
||||||
|
### 1. Build the Widget (if not already built)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm install # Only needed once
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start/Restart ComfyUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your ComfyUI root directory
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add the Demo Node
|
||||||
|
|
||||||
|
1. Open ComfyUI in your browser (usually http://localhost:8188)
|
||||||
|
2. Right-click on the canvas → **Add Node**
|
||||||
|
3. Navigate to: **loramanager** → **demo** → **LoRA Manager Demo (Vue)**
|
||||||
|
4. The node should appear with a Vue-powered widget inside
|
||||||
|
|
||||||
|
### 4. Test the Widget
|
||||||
|
|
||||||
|
The widget provides an interactive demo:
|
||||||
|
|
||||||
|
1. **Enter a model name** in the text field (e.g., "test-lora-model")
|
||||||
|
2. **Adjust the strength** using the number input or +/- buttons (0.0 - 2.0)
|
||||||
|
3. **Click "Apply"** to set the configuration
|
||||||
|
4. A card will appear showing the current configuration
|
||||||
|
5. **Click "Reset"** to clear everything
|
||||||
|
|
||||||
|
### 5. Test Workflow Integration
|
||||||
|
|
||||||
|
1. Add some input/output nodes to create a minimal workflow
|
||||||
|
2. Connect the demo node outputs to other nodes:
|
||||||
|
- `model_name` → Can connect to any STRING input
|
||||||
|
- `strength` → Can connect to any FLOAT input
|
||||||
|
- `info` → Informational STRING output
|
||||||
|
3. Click **Queue Prompt** to execute the workflow
|
||||||
|
4. Check the console/terminal - you should see:
|
||||||
|
```
|
||||||
|
[LoraManagerDemoNode] Vue Widget Demo - Model: test-lora-model, Strength: 1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Test State Persistence
|
||||||
|
|
||||||
|
1. Configure the widget (set model name and strength, click Apply)
|
||||||
|
2. Save the workflow (Ctrl+S / Cmd+S)
|
||||||
|
3. Reload the page
|
||||||
|
4. Load the saved workflow
|
||||||
|
5. The widget should restore its state
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
✅ **Success Indicators:**
|
||||||
|
- Widget appears inside the node with proper styling
|
||||||
|
- PrimeVue components are rendered correctly
|
||||||
|
- Buttons respond to clicks
|
||||||
|
- Input values update reactively
|
||||||
|
- Configuration card appears after clicking Apply
|
||||||
|
- Node outputs the correct data when workflow executes
|
||||||
|
- State persists when saving/loading workflows
|
||||||
|
|
||||||
|
❌ **Common Issues:**
|
||||||
|
|
||||||
|
**Widget doesn't appear:**
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
- Verify `web/comfyui/vue-widgets/demo-widget.js` exists
|
||||||
|
- Restart ComfyUI completely
|
||||||
|
|
||||||
|
**Build errors:**
|
||||||
|
- Make sure you're in the `vue-widgets` directory when running npm commands
|
||||||
|
- Check Node.js version: `node --version` (should be 18+)
|
||||||
|
- Try deleting `node_modules` and running `npm install` again
|
||||||
|
|
||||||
|
**Widget shows but crashes:**
|
||||||
|
- Check browser console for errors
|
||||||
|
- Verify PrimeVue components are imported correctly
|
||||||
|
- Check that the widget type matches between Python and JavaScript
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
For active development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Watch mode for auto-rebuild
|
||||||
|
cd vue-widgets
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Terminal 2: ComfyUI server
|
||||||
|
cd ../../.. # Back to ComfyUI root
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
When you make changes to Vue files:
|
||||||
|
1. Vite automatically rebuilds
|
||||||
|
2. Hard refresh the browser (Ctrl+Shift+R / Cmd+Shift+R)
|
||||||
|
3. Changes should appear
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that the demo works, you can:
|
||||||
|
|
||||||
|
1. **Modify the demo widget** to add more features
|
||||||
|
2. **Create new widgets** for actual LoRA Manager functionality
|
||||||
|
3. **Add more PrimeVue components** (see [PrimeVue Docs](https://primevue.org/))
|
||||||
|
4. **Integrate with the LoRA Manager API** to fetch real data
|
||||||
|
5. **Add styling** to match ComfyUI's theme better
|
||||||
|
|
||||||
|
## File Structure Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
ComfyUI-Lora-Manager/
|
||||||
|
├── vue-widgets/ # Vue source code
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main.ts # Extension registration
|
||||||
|
│ │ └── components/
|
||||||
|
│ │ └── DemoWidget.vue # Demo widget component
|
||||||
|
│ ├── package.json # Dependencies
|
||||||
|
│ ├── vite.config.mts # Build config
|
||||||
|
│ ├── tsconfig.json # TypeScript config
|
||||||
|
│ ├── README.md # Development guide
|
||||||
|
│ └── DEMO_INSTRUCTIONS.md # This file
|
||||||
|
│
|
||||||
|
├── web/comfyui/
|
||||||
|
│ └── vue-widgets/ # Build output (gitignored)
|
||||||
|
│ ├── demo-widget.js # Compiled JavaScript
|
||||||
|
│ └── assets/
|
||||||
|
│ └── demo-widget-*.css # Compiled CSS
|
||||||
|
│
|
||||||
|
├── py/nodes/
|
||||||
|
│ └── demo_vue_widget_node.py # Python node definition
|
||||||
|
│
|
||||||
|
├── __init__.py # Updated with demo node
|
||||||
|
├── VUE_WIDGETS_SETUP.md # Complete setup guide
|
||||||
|
└── .gitignore # Updated to ignore build output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the browser console for errors
|
||||||
|
2. Check the ComfyUI terminal for Python errors
|
||||||
|
3. Review `VUE_WIDGETS_SETUP.md` for detailed documentation
|
||||||
|
4. Review `vue-widgets/README.md` for development guide
|
||||||
169
vue-widgets/README.md
Normal file
169
vue-widgets/README.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Vue Widgets for ComfyUI LoRA Manager
|
||||||
|
|
||||||
|
This directory contains the source code for Vue 3 + PrimeVue custom widgets for ComfyUI LoRA Manager.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
vue-widgets/
|
||||||
|
├── src/ # TypeScript/Vue source code
|
||||||
|
│ ├── main.ts # Main entry point that registers extensions
|
||||||
|
│ └── components/ # Vue components
|
||||||
|
│ └── DemoWidget.vue # Example demo widget
|
||||||
|
├── package.json # Dependencies and build scripts
|
||||||
|
├── vite.config.mts # Vite build configuration
|
||||||
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vue-widgets
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This compiles the TypeScript/Vue code and outputs to `../web/comfyui/vue-widgets/`.
|
||||||
|
|
||||||
|
### Development Mode (Watch)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds the widgets in watch mode, automatically rebuilding when files change.
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a New Widget
|
||||||
|
|
||||||
|
### 1. Create the Python Node
|
||||||
|
|
||||||
|
Create a new node file in `/py/nodes/your_node.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class YourCustomNode:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"your_widget_name": ("YOUR_WIDGET_TYPE", {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
FUNCTION = "process"
|
||||||
|
CATEGORY = "loramanager"
|
||||||
|
|
||||||
|
def process(self, your_widget_name):
|
||||||
|
# Process widget data
|
||||||
|
return (str(your_widget_name),)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"YourCustomNode": YourCustomNode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the Vue Component
|
||||||
|
|
||||||
|
Create a new component in `src/components/YourWidget.vue`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="your-widget-container">
|
||||||
|
<!-- Your UI here using PrimeVue components -->
|
||||||
|
<Button label="Click me" @click="handleClick" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
widget: { serializeValue?: (node: unknown, index: number) => Promise<unknown> }
|
||||||
|
node: { id: number }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Serialize widget data when workflow is saved
|
||||||
|
props.widget.serializeValue = async () => {
|
||||||
|
return { /* your data */ }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Your styles */
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register the Widget
|
||||||
|
|
||||||
|
In `src/main.ts`, add your widget registration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import YourWidget from '@/components/YourWidget.vue'
|
||||||
|
|
||||||
|
// In getCustomWidgets()
|
||||||
|
YOUR_WIDGET_TYPE(node) {
|
||||||
|
return createVueWidget(node, YourWidget, 'your-widget-name')
|
||||||
|
}
|
||||||
|
|
||||||
|
// In nodeCreated()
|
||||||
|
if (node.constructor?.comfyClass !== 'YourCustomNode') return
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Register the Node
|
||||||
|
|
||||||
|
Add your node to `__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .py.nodes.your_node import YourCustomNode
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
# ...
|
||||||
|
"YourCustomNode": YourCustomNode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Build and Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart ComfyUI and test your new widget!
|
||||||
|
|
||||||
|
## Available PrimeVue Components
|
||||||
|
|
||||||
|
This project uses PrimeVue 4.x. Popular components include:
|
||||||
|
|
||||||
|
- `Button` - Buttons with icons and variants
|
||||||
|
- `InputText` - Text input fields
|
||||||
|
- `InputNumber` - Number input with increment/decrement
|
||||||
|
- `Dropdown` - Select dropdowns
|
||||||
|
- `Card` - Card containers
|
||||||
|
- `DataTable` - Data tables with sorting/filtering
|
||||||
|
- `Dialog` - Modal dialogs
|
||||||
|
- `Tree` - Tree view components
|
||||||
|
- And many more! See [PrimeVue Docs](https://primevue.org/)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Build output goes to `../web/comfyui/vue-widgets/` (gitignored)
|
||||||
|
- The widget type name in Python (e.g., "YOUR_WIDGET_TYPE") must match the key in `getCustomWidgets()`
|
||||||
|
- Widget data is serialized when the workflow is saved/executed via `serializeValue()`
|
||||||
|
- ComfyUI's app.js is marked as external and not bundled
|
||||||
1694
vue-widgets/package-lock.json
generated
Normal file
1694
vue-widgets/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
vue-widgets/package.json
Normal file
28
vue-widgets/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "comfyui-lora-manager-vue-widgets",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Vue-based custom widgets for ComfyUI LoRA Manager",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^9.14.0",
|
||||||
|
"primevue": "^4.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@comfyorg/comfyui-frontend-types": "^1.35.4",
|
||||||
|
"@types/node": "^22.10.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vue-tsc": "^2.1.10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:production": "vite build --mode production",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"clean": "rm -rf ../web/comfyui/vue-widgets",
|
||||||
|
"rebuild": "npm run clean && npm run build",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
vue-widgets/pre-commit.example
Normal file
37
vue-widgets/pre-commit.example
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Example pre-commit hook to ensure Vue widgets are built before committing
|
||||||
|
#
|
||||||
|
# To use this hook:
|
||||||
|
# 1. Copy this file to .git/hooks/pre-commit
|
||||||
|
# 2. Make it executable: chmod +x .git/hooks/pre-commit
|
||||||
|
#
|
||||||
|
# Or use a tool like Husky for automatic hook management:
|
||||||
|
# npm install --save-dev husky
|
||||||
|
# npx husky install
|
||||||
|
# npx husky add .git/hooks/pre-commit "cd vue-widgets && npm run build"
|
||||||
|
|
||||||
|
echo "Running pre-commit hook: Building Vue widgets..."
|
||||||
|
|
||||||
|
# Navigate to vue-widgets directory and build
|
||||||
|
cd "$(git rev-parse --show-toplevel)/vue-widgets" || exit 1
|
||||||
|
|
||||||
|
# Check if node_modules exists
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "node_modules not found, running npm install..."
|
||||||
|
npm install || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build Vue widgets
|
||||||
|
npm run build || {
|
||||||
|
echo "❌ Vue widget build failed! Commit aborted."
|
||||||
|
echo "Please fix the build errors before committing."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add built files to the commit
|
||||||
|
cd ..
|
||||||
|
git add web/comfyui/vue-widgets/
|
||||||
|
|
||||||
|
echo "✓ Vue widgets built and staged successfully"
|
||||||
|
exit 0
|
||||||
176
vue-widgets/src/components/DemoWidget.vue
Normal file
176
vue-widgets/src/components/DemoWidget.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div class="demo-widget-container">
|
||||||
|
<h3 class="demo-title">LoRA Manager Demo Widget</h3>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="demo-input">Model Name:</label>
|
||||||
|
<InputText
|
||||||
|
id="demo-input"
|
||||||
|
v-model="modelName"
|
||||||
|
placeholder="Enter model name..."
|
||||||
|
class="demo-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="strength-input">Strength:</label>
|
||||||
|
<InputNumber
|
||||||
|
id="strength-input"
|
||||||
|
v-model="strength"
|
||||||
|
:min="0"
|
||||||
|
:max="2"
|
||||||
|
:step="0.1"
|
||||||
|
showButtons
|
||||||
|
class="demo-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<Button
|
||||||
|
label="Apply"
|
||||||
|
icon="pi pi-check"
|
||||||
|
@click="handleApply"
|
||||||
|
severity="success"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Reset"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
@click="handleReset"
|
||||||
|
severity="secondary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card v-if="appliedValue" class="result-card">
|
||||||
|
<template #title>Current Configuration</template>
|
||||||
|
<template #content>
|
||||||
|
<p><strong>Model:</strong> {{ appliedValue.modelName || 'None' }}</p>
|
||||||
|
<p><strong>Strength:</strong> {{ appliedValue.strength }}</p>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import InputNumber from 'primevue/inputnumber'
|
||||||
|
import Card from 'primevue/card'
|
||||||
|
|
||||||
|
interface ComponentWidget {
|
||||||
|
serializeValue?: (node: unknown, index: number) => Promise<unknown>
|
||||||
|
value?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
widget: ComponentWidget
|
||||||
|
node: { id: number }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelName = ref('')
|
||||||
|
const strength = ref(1.0)
|
||||||
|
const appliedValue = ref<{ modelName: string; strength: number } | null>(null)
|
||||||
|
|
||||||
|
function handleApply() {
|
||||||
|
appliedValue.value = {
|
||||||
|
modelName: modelName.value,
|
||||||
|
strength: strength.value
|
||||||
|
}
|
||||||
|
console.log('Applied configuration:', appliedValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
modelName.value = ''
|
||||||
|
strength.value = 1.0
|
||||||
|
appliedValue.value = null
|
||||||
|
console.log('Reset configuration')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Serialize the widget value when the workflow is saved or executed
|
||||||
|
props.widget.serializeValue = async () => {
|
||||||
|
const value = appliedValue.value || { modelName: '', strength: 1.0 }
|
||||||
|
console.log('Serializing widget value:', value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore widget value if it exists
|
||||||
|
if (props.widget.value) {
|
||||||
|
const savedValue = props.widget.value as { modelName: string; strength: number }
|
||||||
|
modelName.value = savedValue.modelName || ''
|
||||||
|
strength.value = savedValue.strength || 1.0
|
||||||
|
appliedValue.value = savedValue
|
||||||
|
console.log('Restored widget value:', savedValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.demo-widget-container {
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--comfy-menu-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-title {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card :deep(.p-card-title) {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card :deep(.p-card-content) {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
vue-widgets/src/main.ts
Normal file
73
vue-widgets/src/main.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { createApp, type App as VueApp } from 'vue'
|
||||||
|
import PrimeVue from 'primevue/config'
|
||||||
|
import DemoWidget from '@/components/DemoWidget.vue'
|
||||||
|
|
||||||
|
// @ts-ignore - ComfyUI external module
|
||||||
|
import { app } from '../../../scripts/app.js'
|
||||||
|
|
||||||
|
const vueApps = new Map<number, VueApp>()
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
function createVueWidget(node) {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.id = `lora-manager-demo-widget-${node.id}`
|
||||||
|
container.style.width = '100%'
|
||||||
|
container.style.height = '100%'
|
||||||
|
container.style.minHeight = '300px'
|
||||||
|
container.style.display = 'flex'
|
||||||
|
container.style.flexDirection = 'column'
|
||||||
|
container.style.overflow = 'hidden'
|
||||||
|
|
||||||
|
const widget = node.addDOMWidget(
|
||||||
|
'lora_demo_widget',
|
||||||
|
'lora-manager-demo',
|
||||||
|
container,
|
||||||
|
{
|
||||||
|
getMinHeight: () => 320,
|
||||||
|
hideOnZoom: false,
|
||||||
|
serialize: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const vueApp = createApp(DemoWidget, {
|
||||||
|
widget,
|
||||||
|
node
|
||||||
|
})
|
||||||
|
|
||||||
|
vueApp.use(PrimeVue)
|
||||||
|
|
||||||
|
vueApp.mount(container)
|
||||||
|
vueApps.set(node.id, vueApp)
|
||||||
|
|
||||||
|
widget.onRemove = () => {
|
||||||
|
const vueApp = vueApps.get(node.id)
|
||||||
|
if (vueApp) {
|
||||||
|
vueApp.unmount()
|
||||||
|
vueApps.delete(node.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { widget }
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: 'comfyui.loramanager.demo',
|
||||||
|
|
||||||
|
getCustomWidgets() {
|
||||||
|
return {
|
||||||
|
// @ts-ignore
|
||||||
|
LORA_DEMO_WIDGET(node) {
|
||||||
|
return createVueWidget(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
nodeCreated(node) {
|
||||||
|
if (node.constructor?.comfyClass !== 'LoraManagerDemoNode') return
|
||||||
|
|
||||||
|
const [oldWidth, oldHeight] = node.size
|
||||||
|
|
||||||
|
node.setSize([Math.max(oldWidth, 350), Math.max(oldHeight, 400)])
|
||||||
|
}
|
||||||
|
})
|
||||||
24
vue-widgets/tsconfig.json
Normal file
24
vue-widgets/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
vue-widgets/tsconfig.node.json
Normal file
10
vue-widgets/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.mts"]
|
||||||
|
}
|
||||||
35
vue-widgets/vite.config.mts
Normal file
35
vue-widgets/vite.config.mts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, './src/main.ts'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: 'demo-widget'
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'../../../scripts/app.js'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
dir: '../web/comfyui/vue-widgets',
|
||||||
|
entryFileNames: 'demo-widget.js',
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name]-[hash][extname]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sourcemap: true,
|
||||||
|
minify: false
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('production')
|
||||||
|
}
|
||||||
|
})
|
||||||
56
web/comfyui/vue-widgets/assets/demo-widget-RA_dFMfB.css
Normal file
56
web/comfyui/vue-widgets/assets/demo-widget-RA_dFMfB.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
.demo-widget-container[data-v-df0cb94d] {
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--comfy-menu-bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.demo-title[data-v-df0cb94d] {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
.demo-content[data-v-df0cb94d] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.input-group[data-v-df0cb94d] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.input-group label[data-v-df0cb94d] {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
.demo-input[data-v-df0cb94d] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.button-group[data-v-df0cb94d] {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.result-card[data-v-df0cb94d] {
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
.result-card[data-v-df0cb94d] .p-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.result-card[data-v-df0cb94d] .p-card-content {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.result-card p[data-v-df0cb94d] {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
11672
web/comfyui/vue-widgets/demo-widget.js
Normal file
11672
web/comfyui/vue-widgets/demo-widget.js
Normal file
File diff suppressed because one or more lines are too long
1
web/comfyui/vue-widgets/demo-widget.js.map
Normal file
1
web/comfyui/vue-widgets/demo-widget.js.map
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user