feat: add sidebar updater for comfyui_o1key
This commit is contained in:
@@ -132,45 +132,28 @@ O1KEY_API_KEY=你的API密钥
|
|||||||
|
|
||||||
## 🔄 更新插件
|
## 🔄 更新插件
|
||||||
|
|
||||||
自动更新脚本**已改为从国内镜像(Gitee)拉取**,国内用户无需科学上网即可更新。
|
### 界面更新
|
||||||
|
|
||||||
### 方法一:自动更新(推荐)⭐
|
在 ComfyUI 左侧功能栏点击「更新」(位于「重启」下方)。按钮会从当前 Git 仓库的 `origin/main` 拉取最新版本。完成后点击「重启」使新版本生效。
|
||||||
|
|
||||||
**Windows 用户:**
|
界面更新需要通过 Git 安装、处于 `main` 分支,且节点包文件没有本地修改。更新仅允许快进,不会覆盖本地修改或删除配置。ZIP 安装、分支分叉或网络连接失败时,界面会显示原因,需要手动处理。
|
||||||
1. 进入插件目录:`ComfyUI\custom_nodes\comfyui_o1key`
|
|
||||||
2. 双击运行 `自动更新插件(win).bat`
|
如果提示依赖列表已变化,请在 ComfyUI 使用的 Python 环境中执行:
|
||||||
3. 等待更新完成
|
|
||||||
4. 重启 ComfyUI
|
|
||||||
|
|
||||||
**Linux/Mac 用户:**
|
|
||||||
```bash
|
```bash
|
||||||
cd ComfyUI/custom_nodes/comfyui_o1key
|
cd ComfyUI/custom_nodes/comfyui_o1key
|
||||||
chmod +x "自动更新插件(mac).sh" # 首次运行需要添加执行权限
|
python -m pip install -r requirements.txt
|
||||||
./"自动更新插件(mac).sh"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方法二:手动更新
|
### 手动更新
|
||||||
|
|
||||||
从 Gitee 镜像拉取(国内推荐):
|
|
||||||
```bash
|
```bash
|
||||||
cd ComfyUI/custom_nodes/comfyui_o1key
|
cd ComfyUI/custom_nodes/comfyui_o1key
|
||||||
git remote get-url gitee &>/dev/null || git remote add gitee https://gitee.com/resonLzy/comfyui_o1key.git
|
git pull --ff-only origin main
|
||||||
git pull gitee main
|
python -m pip install -r requirements.txt
|
||||||
pip install -r requirements.txt --upgrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
从 GitHub 拉取:
|
更新保留环境变量中配置的 API 密钥。启动时仍会检查是否有新版本。
|
||||||
```bash
|
|
||||||
cd ComfyUI/custom_nodes/comfyui_o1key
|
|
||||||
git pull origin main
|
|
||||||
pip install -r requirements.txt --upgrade
|
|
||||||
```
|
|
||||||
|
|
||||||
**💡 提示:**
|
|
||||||
- 自动更新脚本会自动备份和恢复你的 `.config` 配置文件
|
|
||||||
- 更新会保留环境变量中配置的 API 密钥
|
|
||||||
- 更新检查在每次启动 ComfyUI 时自动进行(不会影响性能)
|
|
||||||
- 如果发现新版本,终端会显示更新提示
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+21
@@ -208,6 +208,10 @@ try:
|
|||||||
from server import PromptServer
|
from server import PromptServer
|
||||||
import folder_paths
|
import folder_paths
|
||||||
from .utils.config import CONFIG_FILE, load_config, NETWORK_ROUTES
|
from .utils.config import CONFIG_FILE, load_config, NETWORK_ROUTES
|
||||||
|
from .utils.updater import UpdateError, update_package
|
||||||
|
import threading as _update_threading
|
||||||
|
|
||||||
|
_update_lock = _update_threading.Lock()
|
||||||
|
|
||||||
def _get_o1key_server_port():
|
def _get_o1key_server_port():
|
||||||
try:
|
try:
|
||||||
@@ -612,6 +616,23 @@ try:
|
|||||||
pass
|
pass
|
||||||
return web.json_response({"success": True, "deleted": deleted_files})
|
return web.json_response({"success": True, "deleted": deleted_files})
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.post("/o1key/update")
|
||||||
|
async def update_node_package(request):
|
||||||
|
if request.headers.get("X-O1Key-Update") != "1":
|
||||||
|
return web.json_response({"error": "无效的更新请求。"}, status=403)
|
||||||
|
if not _update_lock.acquire(blocking=False):
|
||||||
|
return web.json_response({"error": "更新正在进行,请稍候。"}, status=409)
|
||||||
|
try:
|
||||||
|
result = await asyncio.to_thread(update_package)
|
||||||
|
return web.json_response(result)
|
||||||
|
except UpdateError as exc:
|
||||||
|
return web.json_response({"error": str(exc)}, status=409)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("o1key update failed")
|
||||||
|
return web.json_response({"error": "更新失败,请查看 ComfyUI 日志。"}, status=500)
|
||||||
|
finally:
|
||||||
|
_update_lock.release()
|
||||||
|
|
||||||
# === AI 聊天代理(流式 SSE 透传) ===
|
# === AI 聊天代理(流式 SSE 透传) ===
|
||||||
@PromptServer.instance.routes.post("/o1key/restart")
|
@PromptServer.instance.routes.post("/o1key/restart")
|
||||||
async def restart_server(request):
|
async def restart_server(request):
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""Git integration checks for the sidebar updater."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
UPDATER_PATH = Path(__file__).resolve().parents[1] / "utils" / "updater.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("o1key_updater_under_test", UPDATER_PATH)
|
||||||
|
updater = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(updater)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdaterTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
root = Path(self.temp.name)
|
||||||
|
self.remote = root / "remote.git"
|
||||||
|
self.author = root / "author"
|
||||||
|
self.install = root / "install"
|
||||||
|
self.git(root, "init", "--bare", str(self.remote))
|
||||||
|
self.git(root, "clone", str(self.remote), str(self.author))
|
||||||
|
self.git(self.author, "config", "user.email", "[email protected]")
|
||||||
|
self.git(self.author, "config", "user.name", "Updater Test")
|
||||||
|
self.git(self.author, "switch", "-c", "main")
|
||||||
|
(self.author / "requirements.txt").write_text("requests>=2\n", encoding="utf-8")
|
||||||
|
(self.author / "version.txt").write_text("1\n", encoding="utf-8")
|
||||||
|
self.commit_and_push()
|
||||||
|
self.git(root, "clone", "--branch", "main", str(self.remote), str(self.install))
|
||||||
|
updater.PLUGIN_DIR = self.install
|
||||||
|
|
||||||
|
def git(self, cwd, *args):
|
||||||
|
return subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True).stdout.strip()
|
||||||
|
|
||||||
|
def commit_and_push(self):
|
||||||
|
self.git(self.author, "add", ".")
|
||||||
|
self.git(self.author, "commit", "-m", "test update")
|
||||||
|
self.git(self.author, "push", "origin", "main")
|
||||||
|
|
||||||
|
def test_fast_forward_and_requirements_change(self):
|
||||||
|
self.assertFalse(updater.update_package()["updated"])
|
||||||
|
(self.author / "version.txt").write_text("2\n", encoding="utf-8")
|
||||||
|
(self.author / "requirements.txt").write_text("requests>=3\n", encoding="utf-8")
|
||||||
|
self.commit_and_push()
|
||||||
|
result = updater.update_package()
|
||||||
|
self.assertTrue(result["updated"])
|
||||||
|
self.assertTrue(result["requirements_changed"])
|
||||||
|
self.assertEqual((self.install / "version.txt").read_text(encoding="utf-8"), "2\n")
|
||||||
|
|
||||||
|
def test_local_changes_are_preserved(self):
|
||||||
|
(self.install / "version.txt").write_text("local\n", encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(updater.UpdateError, "本地修改"):
|
||||||
|
updater.update_package()
|
||||||
|
self.assertEqual((self.install / "version.txt").read_text(encoding="utf-8"), "local\n")
|
||||||
|
|
||||||
|
def test_diverged_branch_is_rejected(self):
|
||||||
|
self.git(self.install, "config", "user.email", "[email protected]")
|
||||||
|
self.git(self.install, "config", "user.name", "Updater Test")
|
||||||
|
(self.install / "version.txt").write_text("local commit\n", encoding="utf-8")
|
||||||
|
self.git(self.install, "add", ".")
|
||||||
|
self.git(self.install, "commit", "-m", "local")
|
||||||
|
(self.author / "version.txt").write_text("remote commit\n", encoding="utf-8")
|
||||||
|
self.commit_and_push()
|
||||||
|
with self.assertRaisesRegex(updater.UpdateError, "已分叉"):
|
||||||
|
updater.update_package()
|
||||||
|
self.assertEqual((self.install / "version.txt").read_text(encoding="utf-8"), "local commit\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Safely fast-forward a Git installation of this node package."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PLUGIN_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _git(*args, timeout=60, check=True):
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||||
|
env["GCM_INTERACTIVE"] = "Never"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", *args],
|
||||||
|
cwd=PLUGIN_DIR,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise UpdateError("未找到 Git,请先安装 Git。") from exc
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
raise UpdateError("Git 操作超时,请检查网络后重试。") from exc
|
||||||
|
if check and result.returncode:
|
||||||
|
detail = (result.stderr or result.stdout).strip().splitlines()
|
||||||
|
raise UpdateError(detail[-1] if detail else "Git 操作失败。")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def update_package():
|
||||||
|
"""Update origin/main without discarding local changes or switching branches."""
|
||||||
|
if not (PLUGIN_DIR / ".git").exists():
|
||||||
|
raise UpdateError("当前节点包不是 Git 安装。请通过 Git 安装后再使用界面更新。")
|
||||||
|
|
||||||
|
branch = _git("symbolic-ref", "--quiet", "--short", "HEAD", check=False)
|
||||||
|
if branch.returncode or branch.stdout.strip() != "main":
|
||||||
|
raise UpdateError("当前不在 main 分支,请手动检查分支后更新。")
|
||||||
|
|
||||||
|
if _git("status", "--porcelain", "--untracked-files=no").stdout.strip():
|
||||||
|
raise UpdateError("节点包有本地修改,请先保存或处理修改后再更新。")
|
||||||
|
|
||||||
|
old_commit = _git("rev-parse", "HEAD").stdout.strip()
|
||||||
|
old_requirements = _git("show", "HEAD:requirements.txt", check=False).stdout
|
||||||
|
_git("fetch", "origin", "main")
|
||||||
|
new_commit = _git("rev-parse", "FETCH_HEAD").stdout.strip()
|
||||||
|
if old_commit == new_commit:
|
||||||
|
return {"updated": False, "version": old_commit[:7], "requirements_changed": False}
|
||||||
|
|
||||||
|
if _git("merge-base", "--is-ancestor", "HEAD", "FETCH_HEAD", check=False).returncode:
|
||||||
|
raise UpdateError("本地与 origin/main 已分叉,无法安全快进。请手动处理。")
|
||||||
|
|
||||||
|
_git("merge", "--ff-only", "FETCH_HEAD")
|
||||||
|
requirements_changed = old_requirements != (PLUGIN_DIR / "requirements.txt").read_text(encoding="utf-8")
|
||||||
|
return {
|
||||||
|
"updated": True,
|
||||||
|
"version": new_commit[:7],
|
||||||
|
"requirements_changed": requirements_changed,
|
||||||
|
}
|
||||||
+70
-32
@@ -3,11 +3,8 @@ import { app } from "../../../scripts/app.js";
|
|||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "o1key.restartButton",
|
name: "o1key.restartButton",
|
||||||
async setup() {
|
async setup() {
|
||||||
let injected = false;
|
|
||||||
|
|
||||||
function inject() {
|
function inject() {
|
||||||
if (injected) return;
|
if (document.querySelector("#o1k-restart-btn") && document.querySelector("#o1k-update-btn")) return;
|
||||||
if (document.querySelector("#o1k-restart-btn")) { injected = true; return; }
|
|
||||||
|
|
||||||
const allBtns = document.querySelectorAll("button, .p-togglebutton, .side-bar-button");
|
const allBtns = document.querySelectorAll("button, .p-togglebutton, .side-bar-button");
|
||||||
let logBtn = null;
|
let logBtn = null;
|
||||||
@@ -20,36 +17,77 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
if (!logBtn || !logBtn.parentNode) return;
|
if (!logBtn || !logBtn.parentNode) return;
|
||||||
|
|
||||||
const btn = logBtn.cloneNode(false);
|
function makeButton(id, label, title, icon) {
|
||||||
btn.id = "o1k-restart-btn";
|
const btn = logBtn.cloneNode(false);
|
||||||
btn.setAttribute("aria-label", "重启");
|
btn.id = id;
|
||||||
btn.title = "重启 ComfyUI";
|
btn.setAttribute("aria-label", label);
|
||||||
|
btn.title = title;
|
||||||
|
const logStyle = window.getComputedStyle(logBtn);
|
||||||
|
btn.style.display = "flex";
|
||||||
|
btn.style.flexDirection = "column";
|
||||||
|
btn.style.alignItems = "center";
|
||||||
|
btn.style.justifyContent = "center";
|
||||||
|
btn.style.gap = logStyle.gap || "4px";
|
||||||
|
const iconSpan = document.createElement("span");
|
||||||
|
iconSpan.innerHTML = icon;
|
||||||
|
const textSpan = document.createElement("span");
|
||||||
|
textSpan.textContent = label;
|
||||||
|
btn.append(iconSpan, textSpan);
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
const logStyle = window.getComputedStyle(logBtn);
|
let restartBtn = document.querySelector("#o1k-restart-btn");
|
||||||
btn.style.display = "flex";
|
if (!restartBtn) {
|
||||||
btn.style.flexDirection = "column";
|
restartBtn = makeButton("o1k-restart-btn", "重启", "重启 ComfyUI",
|
||||||
btn.style.alignItems = "center";
|
`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>`);
|
||||||
btn.style.justifyContent = "center";
|
restartBtn.addEventListener("click", async () => {
|
||||||
btn.style.gap = logStyle.gap || "4px";
|
if (!confirm("确定要重启 ComfyUI 吗?")) return;
|
||||||
|
restartBtn.style.opacity = "0.5";
|
||||||
|
restartBtn.style.pointerEvents = "none";
|
||||||
|
await disableExperimentalAssetApi();
|
||||||
|
try { await fetch("/o1key/restart", { method: "POST" }); } catch {}
|
||||||
|
pollUntilReady();
|
||||||
|
});
|
||||||
|
logBtn.parentNode.insertBefore(restartBtn, logBtn);
|
||||||
|
}
|
||||||
|
|
||||||
const iconSpan = document.createElement("span");
|
if (!document.querySelector("#o1k-update-btn")) {
|
||||||
iconSpan.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>`;
|
const updateBtn = makeButton("o1k-update-btn", "更新", "更新 comfyui_o1key 节点包",
|
||||||
const textSpan = document.createElement("span");
|
`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M4 18v3h16v-3"/></svg>`);
|
||||||
textSpan.textContent = "重启";
|
let updating = false;
|
||||||
btn.appendChild(iconSpan);
|
updateBtn.addEventListener("click", async () => {
|
||||||
btn.appendChild(textSpan);
|
if (updating) return;
|
||||||
|
if (!confirm("从 origin/main 拉取 comfyui_o1key 最新版本?")) return;
|
||||||
btn.addEventListener("click", async () => {
|
updating = true;
|
||||||
if (!confirm("确定要重启 ComfyUI 吗?")) return;
|
updateBtn.disabled = true;
|
||||||
btn.style.opacity = "0.5";
|
updateBtn.style.opacity = "0.5";
|
||||||
btn.style.pointerEvents = "none";
|
updateBtn.title = "正在更新...";
|
||||||
await disableExperimentalAssetApi();
|
try {
|
||||||
try { await fetch("/o1key/restart", { method: "POST" }); } catch {}
|
const response = await fetch("/o1key/update", {
|
||||||
pollUntilReady();
|
method: "POST",
|
||||||
});
|
headers: { "X-O1Key-Update": "1" },
|
||||||
|
});
|
||||||
logBtn.parentNode.insertBefore(btn, logBtn);
|
const result = await response.json();
|
||||||
injected = true;
|
if (!response.ok) throw new Error(result.error || "更新失败");
|
||||||
|
if (!result.updated) {
|
||||||
|
alert(`已是最新版本(${result.version})。`);
|
||||||
|
} else {
|
||||||
|
const dependencies = result.requirements_changed
|
||||||
|
? "\n依赖列表已变化,请先在 ComfyUI 的 Python 环境中执行 pip install -r requirements.txt。"
|
||||||
|
: "";
|
||||||
|
alert(`更新完成(${result.version})。${dependencies}\n请点击“重启”使新版本生效。`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`更新失败:${error.message}`);
|
||||||
|
} finally {
|
||||||
|
updating = false;
|
||||||
|
updateBtn.disabled = false;
|
||||||
|
updateBtn.style.opacity = "";
|
||||||
|
updateBtn.title = "更新 comfyui_o1key 节点包";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
restartBtn.after(updateBtn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disableExperimentalAssetApi() {
|
async function disableExperimentalAssetApi() {
|
||||||
|
|||||||
Reference in New Issue
Block a user