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 用户:**
|
||||
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
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
+56
-18
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user