From 5fccb3e8eb618af38da8803ac41c353e0b5e1725 Mon Sep 17 00:00:00 2001
From: Jony <951565127@qq.com>
Date: Thu, 24 Sep 2026 17:51:05 +0800
Subject: [PATCH] feat: add sidebar updater for comfyui_o1key
---
README.md | 37 ++++-----------
__init__.py | 21 +++++++++
tests/test_updater.py | 73 ++++++++++++++++++++++++++++
utils/updater.py | 68 +++++++++++++++++++++++++++
web/js/restartButton.js | 102 +++++++++++++++++++++++++++-------------
5 files changed, 242 insertions(+), 59 deletions(-)
create mode 100644 tests/test_updater.py
create mode 100644 utils/updater.py
diff --git a/README.md b/README.md
index 9eaa3f1..5c950c0 100644
--- a/README.md
+++ b/README.md
@@ -132,45 +132,28 @@ O1KEY_API_KEY=你的API密钥
## 🔄 更新插件
-自动更新脚本**已改为从国内镜像(Gitee)拉取**,国内用户无需科学上网即可更新。
+### 界面更新
-### 方法一:自动更新(推荐)⭐
+在 ComfyUI 左侧功能栏点击「更新」(位于「重启」下方)。按钮会从当前 Git 仓库的 `origin/main` 拉取最新版本。完成后点击「重启」使新版本生效。
-**Windows 用户:**
-1. 进入插件目录:`ComfyUI\custom_nodes\comfyui_o1key`
-2. 双击运行 `自动更新插件(win).bat`
-3. 等待更新完成
-4. 重启 ComfyUI
+界面更新需要通过 Git 安装、处于 `main` 分支,且节点包文件没有本地修改。更新仅允许快进,不会覆盖本地修改或删除配置。ZIP 安装、分支分叉或网络连接失败时,界面会显示原因,需要手动处理。
+
+如果提示依赖列表已变化,请在 ComfyUI 使用的 Python 环境中执行:
-**Linux/Mac 用户:**
```bash
cd ComfyUI/custom_nodes/comfyui_o1key
-chmod +x "自动更新插件(mac).sh" # 首次运行需要添加执行权限
-./"自动更新插件(mac).sh"
+python -m pip install -r requirements.txt
```
-### 方法二:手动更新
+### 手动更新
-从 Gitee 镜像拉取(国内推荐):
```bash
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 gitee main
-pip install -r requirements.txt --upgrade
+git pull --ff-only origin main
+python -m pip install -r requirements.txt
```
-从 GitHub 拉取:
-```bash
-cd ComfyUI/custom_nodes/comfyui_o1key
-git pull origin main
-pip install -r requirements.txt --upgrade
-```
-
-**💡 提示:**
-- 自动更新脚本会自动备份和恢复你的 `.config` 配置文件
-- 更新会保留环境变量中配置的 API 密钥
-- 更新检查在每次启动 ComfyUI 时自动进行(不会影响性能)
-- 如果发现新版本,终端会显示更新提示
+更新保留环境变量中配置的 API 密钥。启动时仍会检查是否有新版本。
---
diff --git a/__init__.py b/__init__.py
index 1e60d60..6cb010e 100644
--- a/__init__.py
+++ b/__init__.py
@@ -208,6 +208,10 @@ try:
from server import PromptServer
import folder_paths
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():
try:
@@ -612,6 +616,23 @@ try:
pass
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 透传) ===
@PromptServer.instance.routes.post("/o1key/restart")
async def restart_server(request):
diff --git a/tests/test_updater.py b/tests/test_updater.py
new file mode 100644
index 0000000..0a715d8
--- /dev/null
+++ b/tests/test_updater.py
@@ -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", "test@example.com")
+ 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", "test@example.com")
+ 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()
diff --git a/utils/updater.py b/utils/updater.py
new file mode 100644
index 0000000..bc2ac9d
--- /dev/null
+++ b/utils/updater.py
@@ -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,
+ }
diff --git a/web/js/restartButton.js b/web/js/restartButton.js
index cf1f5cd..6066d59 100644
--- a/web/js/restartButton.js
+++ b/web/js/restartButton.js
@@ -3,11 +3,8 @@ import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "o1key.restartButton",
async setup() {
- let injected = false;
-
function inject() {
- if (injected) return;
- if (document.querySelector("#o1k-restart-btn")) { injected = true; return; }
+ if (document.querySelector("#o1k-restart-btn") && document.querySelector("#o1k-update-btn")) return;
const allBtns = document.querySelectorAll("button, .p-togglebutton, .side-bar-button");
let logBtn = null;
@@ -20,36 +17,77 @@ app.registerExtension({
}
if (!logBtn || !logBtn.parentNode) return;
- const btn = logBtn.cloneNode(false);
- btn.id = "o1k-restart-btn";
- btn.setAttribute("aria-label", "重启");
- btn.title = "重启 ComfyUI";
+ function makeButton(id, label, title, icon) {
+ const btn = logBtn.cloneNode(false);
+ btn.id = id;
+ 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);
- btn.style.display = "flex";
- btn.style.flexDirection = "column";
- btn.style.alignItems = "center";
- btn.style.justifyContent = "center";
- btn.style.gap = logStyle.gap || "4px";
+ let restartBtn = document.querySelector("#o1k-restart-btn");
+ if (!restartBtn) {
+ restartBtn = makeButton("o1k-restart-btn", "重启", "重启 ComfyUI",
+ ``);
+ restartBtn.addEventListener("click", async () => {
+ 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");
- iconSpan.innerHTML = ``;
- const textSpan = document.createElement("span");
- textSpan.textContent = "重启";
- btn.appendChild(iconSpan);
- btn.appendChild(textSpan);
-
- btn.addEventListener("click", async () => {
- if (!confirm("确定要重启 ComfyUI 吗?")) return;
- btn.style.opacity = "0.5";
- btn.style.pointerEvents = "none";
- await disableExperimentalAssetApi();
- try { await fetch("/o1key/restart", { method: "POST" }); } catch {}
- pollUntilReady();
- });
-
- logBtn.parentNode.insertBefore(btn, logBtn);
- injected = true;
+ if (!document.querySelector("#o1k-update-btn")) {
+ const updateBtn = makeButton("o1k-update-btn", "更新", "更新 comfyui_o1key 节点包",
+ ``);
+ let updating = false;
+ updateBtn.addEventListener("click", async () => {
+ if (updating) return;
+ if (!confirm("从 origin/main 拉取 comfyui_o1key 最新版本?")) return;
+ updating = true;
+ updateBtn.disabled = true;
+ updateBtn.style.opacity = "0.5";
+ updateBtn.title = "正在更新...";
+ try {
+ const response = await fetch("/o1key/update", {
+ method: "POST",
+ headers: { "X-O1Key-Update": "1" },
+ });
+ const result = await response.json();
+ 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() {