feat: add sidebar updater for comfyui_o1key

This commit is contained in:
Jony
2026-09-24 17:51:05 +08:00
parent 30e9603f77
commit 5fccb3e8eb
5 changed files with 242 additions and 59 deletions
+10 -27
View File
@@ -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 密钥。启动时仍会检查是否有新版本。
---
+21
View File
@@ -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):
+73
View File
@@ -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()
+68
View File
@@ -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,
}
+56 -18
View File
@@ -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;
function makeButton(id, label, title, icon) {
const btn = logBtn.cloneNode(false);
btn.id = "o1k-restart-btn";
btn.setAttribute("aria-label", "重启");
btn.title = "重启 ComfyUI";
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 = `<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>`;
iconSpan.innerHTML = icon;
const textSpan = document.createElement("span");
textSpan.textContent = "重启";
btn.appendChild(iconSpan);
btn.appendChild(textSpan);
textSpan.textContent = label;
btn.append(iconSpan, textSpan);
return btn;
}
btn.addEventListener("click", async () => {
let restartBtn = document.querySelector("#o1k-restart-btn");
if (!restartBtn) {
restartBtn = makeButton("o1k-restart-btn", "重启", "重启 ComfyUI",
`<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>`);
restartBtn.addEventListener("click", async () => {
if (!confirm("确定要重启 ComfyUI 吗?")) return;
btn.style.opacity = "0.5";
btn.style.pointerEvents = "none";
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);
}
logBtn.parentNode.insertBefore(btn, logBtn);
injected = true;
if (!document.querySelector("#o1k-update-btn")) {
const updateBtn = makeButton("o1k-update-btn", "更新", "更新 comfyui_o1key 节点包",
`<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>`);
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() {