diff --git a/.codewhale/instructions.md b/.codewhale/instructions.md
new file mode 100644
index 0000000..7be8297
--- /dev/null
+++ b/.codewhale/instructions.md
@@ -0,0 +1,21 @@
+# Project Context (Auto-generated)
+
+> This file was automatically generated by CodeWhale.
+> You can edit or delete it at any time.
+
+## Bounded Project Overview
+
+```json
+{
+ "project_name": "xpusys",
+ "directory_structure": [],
+ "readme": null,
+ "config_files": [],
+ "key_source_files": [],
+ "counts": {
+ "config_files": 0,
+ "directory_entries": 0,
+ "key_source_files": 0
+ }
+}
+```
\ No newline at end of file
diff --git a/.deepseek/state/subagents.v1.json b/.deepseek/state/subagents.v1.json
new file mode 100644
index 0000000..70061f2
--- /dev/null
+++ b/.deepseek/state/subagents.v1.json
@@ -0,0 +1,30 @@
+{
+ "schema_version": 1,
+ "agents": [
+ {
+ "id": "agent_81924c2d",
+ "session_name": "clone-fork",
+ "fork_context": false,
+ "workspace": "/home/pe1085/development/xpusys",
+ "agent_type": "implementer",
+ "prompt": "Clone the repository https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD into the current directory /home/pe1085/development/xpusys. Use `git clone https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD .` to clone into the existing directory.",
+ "assignment": {
+ "objective": "Clone the repository https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD into the current directory /home/pe1085/development/xpusys. Use `git clone https://github.com/forkless/ComfyUI-XPUSYS-Monitor-AMD .` to clone into the existing directory.",
+ "role": "implementer"
+ },
+ "model": "deepseek-v4-flash",
+ "nickname": "Beaked",
+ "status": {
+ "Failed": "Sub-agent requested unavailable tools: exec_shell\n(child model `deepseek-v4-flash` may be unavailable under the current access profile — retry agent_open with a different `model`, or remove `model` to inherit the parent's)"
+ },
+ "result": null,
+ "steps_taken": 0,
+ "duration_ms": 17,
+ "allowed_tools": [
+ "exec_shell"
+ ],
+ "updated_at_ms": 1780951716988,
+ "session_boot_id": "boot_3aa40f1d-b3c"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/providers/__init__.py b/providers/__init__.py
index 11268a6..6351cff 100644
--- a/providers/__init__.py
+++ b/providers/__init__.py
@@ -137,13 +137,23 @@ def _is_amd_rocme() -> bool:
"""
Check if torch.cuda is backed by AMD ROCm.
- Returns True if torch.version.roc is not None (AMD ROCm PyTorch).
- Returns False if torch.version.roc is None (NVIDIA or standard CUDA).
+ Primary signal: torch.version.roc is not None (AMD ROCm PyTorch).
+ Fallback signal: GPU device name contains AMD/Radeon markers
+ (catches Windows ROCm builds that don't set version.roc).
+
+ Returns False for NVIDIA or standard CUDA.
"""
try:
import torch
- # torch.version.roc: True/str = AMD ROCm, None = NVIDIA/other
- return torch.version.roc is not None
+ # Primary: ROCm version string
+ if torch.version.roc is not None:
+ return True
+ # Fallback: check GPU name for AMD identifiers
+ name = torch.cuda.get_device_name(0).lower()
+ for marker in ["amd", "radeon", "advanced micro devices"]:
+ if marker in name:
+ return True
+ return False
except Exception:
return False
diff --git a/web/xpu_monitor.js b/web/xpu_monitor.js
new file mode 100644
index 0000000..22363a6
--- /dev/null
+++ b/web/xpu_monitor.js
@@ -0,0 +1,1293 @@
+/**
+ * xpu_monitor.js — ComfyUI-XPUSYS-Monitor
+ *
+ * Bar layout:
+ * [ PRED x.xx/x.xGB xx% ] [ CPU x% @ x.xGHz ] [ RAM x% ] | GPU x% @ xMHz | x°C VRAM x/xGB RSV xGB PWR xW x% |
+ * └── .vram-predictor-section ──┘ └──────────────── .gpu-composite-group ────────────────┘
+ */
+
+import { app } from "../../scripts/app.js";
+import { api } from "../../scripts/api.js";
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+
+const NS = "XPUSYS_Mon";
+const VERSION = "1.0.3";
+const GITHUB = "https://github.com/allanmeng/ComfyUI-XPUSYS-Monitor";
+const S = {
+ lang: `${NS}.Language`,
+ fontSize: `${NS}.FontSize`,
+ refreshMs: `${NS}.RefreshInterval`,
+ showPredictor: `${NS}.ShowPredictor`,
+ showCPU: `${NS}.ShowCPU`,
+ showRAM: `${NS}.ShowRAM`,
+ showEngine: `${NS}.ShowEngine`,
+ showVRAM: `${NS}.ShowVRAM`,
+ showRSV: `${NS}.ShowRSV`,
+ showPower: `${NS}.ShowPower`,
+};
+
+// Intel Arc PCI device ID → spec TBP (W) — Intel ARK / product pages / NotebookCheck
+// Only includes cards with practical AI inference capability (≥8 GB VRAM or workstation Pro series)
+const ARC_PCI_TGP = {
+ // ── Battlemage (Xe2) — B series consumer ─────────────────────────────────
+ "0xe20b": 190, // Arc B580 (desktop, 12 GB)
+ "0xe20a": 190, // Arc B770 (desktop, 16 GB, announced)
+ "0xe20c": 150, // Arc B570 (desktop, 10 GB)
+ "0xe208": 120, // Arc B580M (mobile, 12 GB)
+ "0xe209": 100, // Arc B570M (mobile, 10 GB)
+
+ // ── Battlemage (Xe2) — B series Pro (workstation) ─────────────────────────
+ "0xe211": 200, // Arc Pro B60 (desktop, 24 GB, TBP 120–200 W, use max)
+ "0xe212": 70, // Arc Pro B50 (desktop, 16 GB)
+
+ // ── Alchemist (Xe-HPG) — A series consumer desktop ───────────────────────
+ "0x56a0": 225, // Arc A770 (desktop, 16 GB)
+ "0x56a1": 150, // Arc A750 (desktop, 8 GB)
+ "0x56a2": 75, // Arc A580 (desktop, 8 GB)
+ "0x56a5": 75, // Arc A380 (desktop, 6 GB — borderline, kept)
+
+ // ── Alchemist (Xe-HPG) — A series consumer mobile ────────────────────────
+ "0x5690": 150, // Arc A770M (120–150 W configurable, use max)
+ "0x5691": 120, // Arc A730M (80–120 W configurable, use max)
+ "0x5696": 80, // Arc A570M (50–80 W configurable, use max)
+ "0x5692": 80, // Arc A550M (60–80 W configurable, use max)
+ "0x5697": 50, // Arc A530M (35–50 W configurable, use max)
+
+ // ── Alchemist (Xe-HPG) — A series Pro (workstation) ──────────────────────
+ "0x56b3": 130, // Arc Pro A60 (desktop, 12 GB)
+ "0x56b2": 75, // Arc Pro A60M (mobile, 8 GB) — device ID: 56B2
+ "0x56b1": 50, // Arc Pro A40/A50 (desktop, 6 GB) — shared device ID
+ "0x56b0": 35, // Arc Pro A30M (mobile, 4 GB)
+};
+
+// Intel Arc PCI device ID → marketing name
+const ARC_PCI_NAMES = {
+ // ── Battlemage (Xe2) — B series consumer ─────────────────────────────────
+ "0xe20b": "Intel Arc B580",
+ "0xe20a": "Intel Arc B770",
+ "0xe20c": "Intel Arc B570",
+ "0xe208": "Intel Arc B580M",
+ "0xe209": "Intel Arc B570M",
+
+ // ── Battlemage (Xe2) — B series Pro (workstation) ─────────────────────────
+ "0xe211": "Intel Arc Pro B60",
+ "0xe212": "Intel Arc Pro B50",
+
+ // ── Alchemist (Xe-HPG) — A series consumer desktop ───────────────────────
+ "0x56a0": "Intel Arc A770",
+ "0x56a1": "Intel Arc A750",
+ "0x56a2": "Intel Arc A580",
+ "0x56a5": "Intel Arc A380",
+
+ // ── Alchemist (Xe-HPG) — A series consumer mobile ────────────────────────
+ "0x5690": "Intel Arc A770M",
+ "0x5691": "Intel Arc A730M",
+ "0x5696": "Intel Arc A570M",
+ "0x5692": "Intel Arc A550M",
+ "0x5697": "Intel Arc A530M",
+
+ // ── Alchemist (Xe-HPG) — A series Pro (workstation) ──────────────────────
+ "0x56b3": "Intel Arc Pro A60",
+ "0x56b2": "Intel Arc Pro A60M",
+ "0x56b1": "Intel Arc Pro A40/A50", // A40 and A50 share the same device ID
+ "0x56b0": "Intel Arc Pro A30M",
+};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function resolveTGP(snap) {
+ // Table (Intel ARK specs) takes priority over backend reading
+ if (snap.device_name) {
+ const m = snap.device_name.match(/\[0x([0-9a-fA-F]+)\]/);
+ if (m) {
+ const key = "0x" + m[1].toLowerCase();
+ if (ARC_PCI_TGP[key]) return ARC_PCI_TGP[key];
+ }
+ }
+ if (snap.tgp_w > 0) return snap.tgp_w;
+ return 0;
+}
+
+function resolveDeviceName(raw) {
+ if (!raw) return "Intel Arc GPU";
+ const m = raw.match(/\[0x([0-9a-fA-F]+)\]/);
+ if (m) {
+ const key = "0x" + m[1].toLowerCase();
+ if (ARC_PCI_NAMES[key]) return ARC_PCI_NAMES[key];
+ }
+ return raw;
+}
+
+function shortDeviceName(raw) {
+ const full = resolveDeviceName(raw);
+ const parts = full.trim().split(/\s+/);
+ return parts[parts.length - 1] || full;
+}
+
+function getSetting(id, def) {
+ try { const v = app.extensionManager?.setting?.get(id); if (v !== undefined) return v; } catch (_) {}
+ try { return app.ui.settings.getSettingValue(id, def); } catch (_) {}
+ return def;
+}
+
+function en() {
+ const manual = getSetting(S.lang, "system");
+ if (manual === "en") return true;
+ if (manual === "zh") return false;
+ // "system" 或未知值 → 跟随 Comfy.Locale
+ const comfyLocale = getSetting("Comfy.Locale", "");
+ if (comfyLocale) return !comfyLocale.toLowerCase().startsWith("zh");
+ return true; // 最终 fallback:英文
+}
+
+
+// 双语辅助:en() 为 true 时返回英文,否则返回中文
+function t(zh, en_str) { return en() ? en_str : zh; }
+
+function makeSliderType(min, max, step, liveUpdate = false) {
+ return (_name, setter, value) => {
+ const wrap = document.createElement("div");
+ wrap.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;";
+
+ const slider = document.createElement("input");
+ slider.type = "range"; slider.min = min; slider.max = max; slider.step = step;
+ slider.value = value ?? min;
+ slider.style.cssText = "flex:1;cursor:pointer;";
+
+ const box = document.createElement("input");
+ box.type = "number"; box.min = min; box.max = max; box.step = step;
+ box.value = value ?? min;
+ box.style.cssText = "width:62px;padding:2px 4px;background:transparent;" +
+ "border:1px solid #555;border-radius:3px;color:inherit;" +
+ "text-align:center;font-size:inherit;";
+
+ slider.addEventListener("input", () => {
+ const c = Math.max(min, Math.min(max, Number(slider.value)));
+ box.value = c;
+ });
+ slider.addEventListener("mouseover", () => {
+ const c = Math.max(min, Math.min(max, Number(slider.value)));
+ box.value = c;
+ if (liveUpdate) setter(c);
+ });
+ slider.addEventListener("change", () => {
+ const c = Math.max(min, Math.min(max, Number(slider.value)));
+ slider.value = c; box.value = c; setter(c);
+ });
+ box.addEventListener("change", () => {
+ const c = Math.max(min, Math.min(max, Number(box.value)));
+ slider.value = c; box.value = c; setter(c);
+ });
+ wrap.appendChild(slider); wrap.appendChild(box);
+ return wrap;
+ };
+}
+
+function makeLangSelectType() {
+ return (_name, setter, value) => {
+ const sel = document.createElement("select");
+ sel.style.cssText = "background:#2a2a2a;border:1px solid #555;border-radius:4px;" +
+ "color:inherit;padding:3px 8px;font-size:inherit;cursor:pointer;";
+ const opts = [
+ { label: "系统", value: "system" },
+ { label: "中文", value: "zh" },
+ { label: "English", value: "en" },
+ ];
+ // 兼容旧版存的 boolean 值
+ const normalized = value === true ? "en" : value === false ? "zh" : (value || "system");
+ opts.forEach(o => {
+ const opt = document.createElement("option");
+ opt.value = o.value;
+ opt.textContent = o.label;
+ if (normalized === o.value) opt.selected = true;
+ sel.appendChild(opt);
+ });
+ sel.addEventListener("change", () => setter(sel.value));
+ return sel;
+ };
+}
+
+// ---------------------------------------------------------------------------
+// CSS
+// ---------------------------------------------------------------------------
+
+function injectStyles() {
+ const style = document.createElement("style");
+ style.textContent = `
+ .xpu-monitor-bar {
+ font-family: 'Consolas', 'JetBrains Mono', 'Cascadia Code', 'PingFang SC', 'Microsoft YaHei', monospace;
+ font-size: var(--xpusys-fs, 18px);
+ font-variant-numeric: tabular-nums;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ gap: 0;
+ padding: 1px 0;
+ user-select: none;
+ white-space: nowrap;
+ }
+ .xpu-monitor-bar > *:first-child { margin-left: 0; }
+
+ /* ── 固定宽度数字槽位 ──
+ 每个 min-width = 该字段最长可能输出的字符数,单位 ch(等宽字体下 1ch = 1字符宽)
+ n-pct : "100.0%" = 6ch
+ n-ghz : "5.2GHz" = 6ch
+ n-mhz : "2450MHz"= 7ch
+ n-gb : "11.9" = 4ch
+ n-temp : "99°C" = 4ch
+ n-w : "190W" = 4ch
+ n-ratio: "100%" = 4ch ── */
+ .n-pct { display: inline-block; min-width: 6ch; text-align: right; }
+ .n-ghz { display: inline-block; min-width: 6ch; text-align: right; }
+ .n-mhz { display: inline-block; min-width: 7ch; text-align: right; }
+ .n-gb { display: inline-block; min-width: 4ch; text-align: right; }
+ .n-temp { display: inline-block; min-width: 4ch; text-align: right; }
+ .n-w { display: inline-block; min-width: 4ch; text-align: right; }
+ .n-ratio { display: inline-block; min-width: 4ch; text-align: right; }
+
+ /* ── 显存预测胶囊 ── */
+ .vram-predictor-section {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: 'Consolas', 'JetBrains Mono', 'Cascadia Code', 'PingFang SC', 'Microsoft YaHei', monospace;
+ font-size: var(--xpusys-fs, 18px); /* 直接读变量,与 bar 联动 */
+ line-height: 1; /* 防止中文字体默认行高撑高胶囊 */
+ font-variant-numeric: tabular-nums;
+ background: rgba(54, 207, 201, 0.10);
+ border: 1px solid rgba(54, 207, 201, 0.22);
+ border-radius: 8px;
+ padding: 10px 0.6em;
+ margin: 0 2px;
+ cursor: default;
+ position: relative;
+ transition: background 0.15s, box-shadow 0.15s;
+ box-shadow:
+ -2px -2px 5px rgba(255, 255, 255, 0.03),
+ 2px 2px 6px rgba(0, 0, 0, 0.5);
+ }
+ .vram-predictor-section:hover {
+ background: rgba(54, 207, 201, 0.18);
+ box-shadow:
+ -2px -2px 5px rgba(255, 255, 255, 0.05),
+ 2px 2px 6px rgba(0, 0, 0, 0.6);
+ }
+ /* 中文标签视觉微调:汉字渲染比等宽英文大,缩至 0.85em */
+ .pred-zh { font-size: 0.85em; }
+ /* 让胶囊内所有 inline 元素(中文/数字/符号)flex 垂直居中,消除基线错位 */
+ .vram-predictor-section .xpusys-value {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ /* ── 通用胶囊段落 ── */
+ .cpu-section, .ram-section {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 8px;
+ padding: 10px 0.6em;
+ margin: 0 1.5px;
+ cursor: default;
+ position: relative;
+ transition: background 0.15s, box-shadow 0.15s;
+ box-shadow:
+ -2px -2px 5px rgba(255, 255, 255, 0.03),
+ 2px 2px 6px rgba(0, 0, 0, 0.5);
+ }
+ /* cpu: "CPU"(3) + n-pct(6) + "@"(1) + n-ghz(6) = 16ch */
+ .cpu-section { min-width: 16ch; }
+ /* ram: "RAM"(3) + n-pct(6) = 9ch */
+ .ram-section { min-width: 9ch; }
+ .cpu-section:hover, .ram-section:hover {
+ background: rgba(255, 255, 255, 0.08);
+ box-shadow:
+ -2px -2px 5px rgba(255, 255, 255, 0.05),
+ 2px 2px 6px rgba(0, 0, 0, 0.6);
+ }
+
+ /* ── GPU 综合体强化 ── */
+ .gpu-composite-group {
+ display: flex;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 8px;
+ padding: 0 4px;
+ margin: 0 1.5px;
+ gap: 0;
+ box-shadow:
+ -2px -2px 5px rgba(255, 255, 255, 0.03),
+ 2px 2px 6px rgba(0, 0, 0, 0.5);
+ }
+
+ /* ── GPU 子项 ── */
+ .gpu-engine, .gpu-vram, .gpu-rsv, .gpu-pwr {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 0.6em;
+ border-right: 1px solid rgba(255, 255, 255, 0.10);
+ cursor: default;
+ position: relative;
+ transition: background 0.15s;
+ }
+ /* engine: "GPU"(3)+n-pct(6)+"@"(1)+n-mhz(7)+"|"(1)+n-temp(4) = 22ch */
+ .gpu-engine { min-width: 22ch; }
+ /* vram: "VRAM"(4)+n-gb(4)+"/"(1)+n-gb(4)+" GB"(3) = 16ch */
+ .gpu-vram { min-width: 16ch; }
+ /* rsv: "RSV"(3)+n-gb(4)+" GB"(3) = 10ch */
+ .gpu-rsv { min-width: 10ch; }
+ /* pwr: "PWR"(3)+n-w(4)+n-ratio(4) = 11ch */
+ .gpu-pwr { min-width: 11ch; }
+ .gpu-pwr { border-right: none; }
+ .gpu-engine:hover, .gpu-vram:hover, .gpu-rsv:hover, .gpu-pwr:hover {
+ background: rgba(255, 255, 255, 0.06);
+ border-radius: 3px;
+ }
+
+ .xpusys-value { font-weight: 600; letter-spacing: 0.02em; }
+ .xpusys-ok { color: #52c41a; }
+ .xpusys-warn { color: #faad14; }
+ .xpusys-critical { color: #ff4d4f; }
+ .xpusys-vram-ok { color: #e8e8e8; }
+ .xpusys-vram-warn{ color: #ff7a00; }
+ .xpusys-vram-crit{ color: #ff4d4f; }
+ .xpusys-pwr-ok { color: #36cfc9; }
+ .xpusys-pwr-warn { color: #b37feb; }
+ .xpusys-pwr-crit { color: #ff4d4f; }
+ .xpusys-na { color: #555; }
+
+ .xpusys-lock {
+ font-size: 10px; color: #666; cursor: pointer;
+ border: 1px solid #444; border-radius: 3px;
+ padding: 0 3px; line-height: 14px; margin-left: 4px;
+ transition: color 0.15s, border-color 0.15s;
+ }
+ .xpusys-lock:hover { color: #aaa; border-color: #aaa; }
+
+ .xpusys-tooltip {
+ position: fixed;
+ background: rgba(18,18,24,0.97);
+ border: 1px solid rgba(255,255,255,0.15);
+ border-radius: 6px;
+ padding: 8px 12px;
+ font-size: 16px;
+ font-family: 'JetBrains Mono', monospace;
+ color: #ccc;
+ line-height: 1.7;
+ pointer-events: none;
+ z-index: 99999;
+ min-width: 220px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.6);
+ white-space: pre;
+ }
+ .xpusys-tooltip-title {
+ color: #fff; font-weight: 700; font-size: 17px; margin-bottom: 4px;
+ border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 3px;
+ }
+ .xpusys-tooltip-row { display: flex; justify-content: space-between; gap: 16px; }
+ .xpusys-tooltip-key { color: #888; }
+ .xpusys-tooltip-val { color: #e8e8e8; font-weight: 600; }
+ .xpusys-tooltip-note{ color: #666; font-size: 15px; margin-top: 4px; }
+ `;
+ document.head.appendChild(style);
+}
+
+// ---------------------------------------------------------------------------
+// Tooltip engine
+// ---------------------------------------------------------------------------
+
+let _tipEl = null;
+let _tipTarget = null;
+
+function createTooltip() {
+ _tipEl = document.createElement("div");
+ _tipEl.className = "xpusys-tooltip";
+ _tipEl.style.display = "none";
+ document.body.appendChild(_tipEl);
+}
+
+function showTooltip(el, html) {
+ _tipTarget = el;
+ _tipEl.innerHTML = html;
+ _tipEl.style.display = "block";
+ positionTooltip(el);
+}
+
+function positionTooltip(el) {
+ const r = el.getBoundingClientRect();
+ const tw = _tipEl.offsetWidth, th = _tipEl.offsetHeight;
+ let x = r.left + r.width / 2 - tw / 2;
+ let y = r.bottom + 6;
+ x = Math.max(6, Math.min(x, window.innerWidth - tw - 6));
+ if (y + th > window.innerHeight - 6) y = r.top - th - 6;
+ _tipEl.style.left = x + "px";
+ _tipEl.style.top = y + "px";
+}
+
+function hideTooltip() {
+ if (_tipEl) _tipEl.style.display = "none";
+ _tipTarget = null;
+}
+
+function tipRow(key, val, color) {
+ const vc = color ? ` style="color:${color}"` : "";
+ return `
` +
+ `${key} ` +
+ `${val} ` +
+ `
`;
+}
+function tipTitle(t) { return `${t}
`; }
+function tipNote(t) { return `${t}
`; }
+
+// ---------------------------------------------------------------------------
+// DOM — bar builder
+// ---------------------------------------------------------------------------
+
+let _sec = {}; // { predictor, cpu, ram, engine, vram, rsv, pwr } each { el, valEl }
+
+// ---------------------------------------------------------------------------
+// Predictor state (model-file scan results from /xpusys/model_sizes)
+// ---------------------------------------------------------------------------
+let _predModels = []; // [{ name: string, size: number }]
+let _predTimer = null; // debounce handle
+
+function makeSection(cls, initText) {
+ const el = document.createElement("div");
+ el.className = cls;
+ const valEl = document.createElement("span");
+ valEl.className = "xpusys-value";
+ valEl.textContent = initText;
+ el.appendChild(valEl);
+ return { el, valEl };
+}
+
+function buildBar() {
+ const bar = document.createElement("div");
+ bar.id = "xpusys-bar";
+ bar.className = "xpu-monitor-bar";
+
+ _sec.predictor = makeSection("vram-predictor-section", "PRED ---");
+ bar.appendChild(_sec.predictor.el);
+
+ _sec.cpu = makeSection("cpu-section", "CPU --.--%");
+ bar.appendChild(_sec.cpu.el);
+
+ _sec.ram = makeSection("ram-section", "RAM --.--%");
+ bar.appendChild(_sec.ram.el);
+
+ const gpuGroup = document.createElement("div");
+ gpuGroup.className = "gpu-composite-group";
+
+ _sec.engine = makeSection("gpu-engine", "GPU ---");
+ _sec.vram = makeSection("gpu-vram", "VRAM ---");
+ _sec.rsv = makeSection("gpu-rsv", "RSV ---");
+ _sec.pwr = makeSection("gpu-pwr", "PWR ---");
+
+ gpuGroup.appendChild(_sec.engine.el);
+ gpuGroup.appendChild(_sec.vram.el);
+ gpuGroup.appendChild(_sec.rsv.el);
+ gpuGroup.appendChild(_sec.pwr.el);
+ bar.appendChild(gpuGroup);
+
+ for (const [key, sec] of Object.entries(_sec)) {
+ sec.el.addEventListener("mouseenter", () => { if (_snap) showTip(key, _snap); });
+ sec.el.addEventListener("mousemove", () => { if (_tipTarget === sec.el && _snap) positionTooltip(sec.el); });
+ sec.el.addEventListener("mouseleave", hideTooltip);
+ }
+
+ return bar;
+}
+
+function mountBar(bar) {
+ if (app.menu?.settingsGroup?.element) {
+ app.menu.settingsGroup.element.before(bar);
+ } else {
+ Object.assign(bar.style, { position: "fixed", top: "6px", right: "8px", zIndex: "9999" });
+ document.body.appendChild(bar);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Render
+// ---------------------------------------------------------------------------
+
+let _snap = null;
+
+function setVal(key, text, cls) {
+ const sec = _sec[key];
+ if (!sec) return;
+ sec.valEl.textContent = text;
+ sec.valEl.className = "xpusys-value " + (cls || "");
+}
+
+function setHTML(key, html, cls) {
+ const sec = _sec[key];
+ if (!sec) return;
+ sec.valEl.innerHTML = html;
+ sec.valEl.className = "xpusys-value " + (cls || "");
+}
+
+function renderSnap(snap) {
+ if (!snap) return;
+ _snap = snap;
+ renderCPU(snap);
+ renderRAM(snap);
+ renderEngine(snap);
+ renderVRAM(snap);
+ renderRSV(snap);
+ renderPWR(snap);
+ renderPredictor(); // re-render with latest vram_total_gb from snap
+ applyVisibility();
+}
+
+function renderCPU(snap) {
+ const pct = snap.cpu_pct ?? 0;
+ const freq = snap.cpu_freq_ghz ?? 0;
+ const cls = pct > 80 ? "xpusys-critical" : pct > 50 ? "xpusys-warn" : "xpusys-ok";
+ let html = `CPU${pct.toFixed(1)}% `;
+ if (freq > 0) html += `@${freq.toFixed(2)}GHz `;
+ setHTML("cpu", html, cls);
+}
+
+function renderRAM(snap) {
+ const pct = snap.ram_pct ?? 0;
+ const cls = pct > 90 ? "xpusys-critical" : pct > 70 ? "xpusys-warn" : "xpusys-ok";
+ setHTML("ram", `RAM${pct.toFixed(1)}% `, cls);
+}
+
+function renderEngine(snap) {
+ const load = snap.gpu_load_pct ?? 0;
+ const freq = snap.gpu_freq_mhz ?? 0;
+ const temp = snap.gpu_temp_c ?? -1;
+ const cls = load > 95 ? "xpusys-critical" : load > 80 ? "xpusys-warn" : "xpusys-ok";
+ let html = `GPU${load.toFixed(1)}% `;
+ if (freq > 0) html += `@${Math.round(freq)}MHz `;
+ if (temp >= 0) html += `|${Math.round(temp)}°C `;
+ setHTML("engine", html, cls);
+}
+
+function renderVRAM(snap) {
+ const used = snap.vram_driver_used_gb ?? 0;
+ const total = snap.vram_total_gb ?? 0;
+ const pct = total > 0 ? used / total : 0;
+ const cls = pct > 0.95 ? "xpusys-vram-crit" : pct > 0.85 ? "xpusys-vram-warn" : "xpusys-vram-ok";
+ setHTML("vram",
+ `VRAM${used.toFixed(1)} /${total.toFixed(1)} GB`,
+ cls);
+}
+
+function renderRSV(snap) {
+ const rsv = snap.vram_reserved_gb ?? 0;
+ setHTML("rsv",
+ `RSV${rsv.toFixed(1)} GB`,
+ rsv > 0.01 ? "xpusys-pwr-warn" : "");
+}
+
+function renderPWR(snap) {
+ const sec = _sec.pwr;
+ if (!sec) return;
+
+ const existing = sec.el.querySelector(".xpusys-lock");
+ if (existing) existing.remove();
+
+ if (!snap.power_available) {
+ setVal("pwr", "PWR N/A", "xpusys-na");
+ const lock = document.createElement("span");
+ lock.className = "xpusys-lock";
+ lock.textContent = "🔒";
+ lock.title = "点击了解详情";
+ lock.addEventListener("click", e => {
+ e.stopPropagation();
+ const dev = shortDeviceName(snap.device_name);
+ const msg = snap.is_admin
+ ? "未找到功率域 — 请检查驱动版本。"
+ : `${dev} 功率数据需要管理员权限。\n\n以管理员身份运行 ComfyUI 即可启用实时功率监控。`;
+ alert("⚡ XPUSYSMonitor — 功率说明\n\n" + msg);
+ });
+ sec.el.appendChild(lock);
+ return;
+ }
+
+ if (snap.power_w < 0) { setVal("pwr", "PWR N/A", "xpusys-na"); return; }
+
+ const tgp = resolveTGP(snap);
+ let pCls, html;
+ if (tgp > 0) {
+ const ratio = snap.power_w / tgp;
+ pCls = ratio > 0.95 ? "xpusys-pwr-crit" : ratio > 0.80 ? "xpusys-pwr-warn" : "xpusys-pwr-ok";
+ html = `PWR${snap.power_w.toFixed(0)}W ${Math.round(ratio * 100)}% `;
+ } else {
+ pCls = snap.power_w > 170 ? "xpusys-pwr-crit" : snap.power_w > 120 ? "xpusys-pwr-warn" : "xpusys-pwr-ok";
+ html = `PWR${snap.power_w.toFixed(0)}W `;
+ }
+ setHTML("pwr", html, pCls);
+}
+
+function renderPredictor() {
+ if (!_sec.predictor) return;
+ const total = _predModels.reduce((s, m) => s + (m.size || 0), 0);
+ const peak = _predModels.length > 0 ? Math.max(..._predModels.map(m => m.size || 0)) : 0;
+ const isEn = en();
+ const pred = calcPrediction(total, peak, _snap);
+ const { rate, color: c, label } = pred;
+ const vEff = pred.vEff.toFixed(1);
+ const risk = isEn ? label.en : label.zh;
+
+ const html = isEn
+ ? `Model:${total.toFixed(2)}G /${vEff}G`
+ + ` | ${risk} `
+ + ` | Success Rate:${rate}% `
+ : `模型体量: ${total.toFixed(2)}G /${vEff}G`
+ + ` | 状态: ${risk} `
+ + ` | 预测工作流执行成功率: ${rate}% `;
+ setHTML("predictor", html);
+}
+
+// ---------------------------------------------------------------------------
+// Predictor — 成功率计算(三级内存模型)
+// ---------------------------------------------------------------------------
+// 常量
+const PRED_ALPHA = 0.9; // 显存碎片化折扣
+
+/**
+ * 双约束成功率预测(串行回收模型)
+ *
+ * 硬约束 P_peak:最大单模型能否装入显存(决定能否运行)
+ * 软约束 P_load:总模型量能否在显存+内存中循环(决定稳定性)
+ * P_success = P_peak × P_load
+ *
+ * @param {number} mTotal 所有模型总大小 (GB)
+ * @param {number} mPeak 最大单个模型大小 (GB)
+ * @param {object} snap 最新系统快照
+ */
+function calcPrediction(mTotal, mPeak, snap) {
+ const vFree = snap?.vram_free_gb ?? 0;
+ const vAlloc = snap?.vram_allocated_gb ?? 0;
+ const vRsv = snap?.vram_reserved_gb ?? 0;
+ const rFree = snap?.ram_free_gb ?? 0;
+ const cUsed = snap?.commit_used_gb ?? 0;
+ const cLimit = snap?.commit_limit_gb ?? 0;
+
+ // 可回收显存 = 空闲 + PyTorch 占用(工作流启动前会释放)
+ const vReclaim = vFree + vAlloc + vRsv;
+ const vEff = Math.max(0.1, vReclaim * PRED_ALPHA);
+ const cRam = rFree; // ram_free_gb 已是 OS 报告的真实空闲量,直接使用
+ const sVirt = Math.max(0, cLimit - cUsed); // 与内存胶囊"虚拟内存"定义一致
+
+ // ── 平台系数:NVIDIA UVM 允许更大显存溢出 ─────────────────────────────
+ const PLATFORM_GAMMA = {
+ "intel": 1.0, // Intel Arc:硬约束严格
+ "nvidia": 4.0, // NVIDIA:UVM 支持约 4x 溢出
+ };
+ const gpuVendor = snap?.gpu_vendor ?? "intel";
+ const gamma = PLATFORM_GAMMA[gpuVendor] ?? 1.0;
+
+ // ── 硬约束:峰值模型 vs 显存 ──────────────────────────────────────────
+ const dPeak = Math.max(0, mPeak - vEff);
+ // 平台差异化:NVIDIA 的 effective 显存按 gamma 倍计算
+ const vEffPlatform = vEff * gamma;
+ const pPeak = dPeak === 0 ? 1 : Math.max(0.02, Math.exp(-3 * dPeak / vEffPlatform));
+
+ // ── 软约束:总量 vs 显存+内存(串行回收) ────────────────────────────
+ const dLoad = Math.max(0, mTotal - vEff);
+ let pLoad;
+ if (dLoad === 0) {
+ pLoad = 1;
+ } else if (cRam > 0 && dLoad <= cRam) {
+ pLoad = 1 - 0.3 * Math.pow(dLoad / cRam, 0.6);
+ } else if (sVirt > 0 && dLoad <= cRam + sVirt) {
+ pLoad = 0.05 + 0.65 * Math.pow(1 - (dLoad - cRam) / sVirt, 2);
+ } else {
+ pLoad = Math.max(0, 0.05 - 0.1 * (dLoad - cRam - sVirt));
+ }
+
+ const rate = Math.max(0, Math.min(100, Math.round(pPeak * pLoad * 100)));
+
+ let color, label;
+ if (rate >= 95) {
+ color = "#52c41a"; label = { zh: "轻松", en: "Smooth" };
+ } else if (rate >= 80) {
+ color = "#afff00"; label = { zh: "安全", en: "Safe" };
+ } else if (rate >= 40) {
+ color = "#faad14"; label = { zh: "预警", en: "Warning" };
+ } else {
+ color = "#ff4d4f"; label = { zh: "危险", en: "Critical" };
+ }
+
+ return { rate, color, label, dPeak, dLoad, vEff, cRam, sVirt, pPeak, pLoad, gamma, gpuVendor };
+}
+
+// ---------------------------------------------------------------------------
+// Visibility & font
+// ---------------------------------------------------------------------------
+
+function applyVisibility() {
+ if (!_sec.cpu) return;
+ if (_sec.predictor)
+ _sec.predictor.el.style.display = getSetting(S.showPredictor, true) ? "" : "none";
+ _sec.cpu.el.style.display = getSetting(S.showCPU, true) ? "" : "none";
+ _sec.ram.el.style.display = getSetting(S.showRAM, true) ? "" : "none";
+ _sec.engine.el.style.display = getSetting(S.showEngine, true) ? "" : "none";
+ _sec.vram.el.style.display = getSetting(S.showVRAM, true) ? "" : "none";
+ _sec.rsv.el.style.display = getSetting(S.showRSV, true) ? "" : "none";
+ _sec.pwr.el.style.display = getSetting(S.showPower, true) ? "" : "none";
+}
+
+function applyFontSize(val) {
+ const px = (val != null && !isNaN(Number(val))) ? Number(val) : Number(getSetting(S.fontSize, 18));
+ document.documentElement.style.setProperty("--xpusys-fs", px + "px");
+}
+
+// ---------------------------------------------------------------------------
+// Tooltip content
+// ---------------------------------------------------------------------------
+
+function showTip(key, snap) {
+ const el = _sec[key]?.el;
+ if (!el) return;
+ const builders = { predictor: buildPredictorTip,
+ cpu: buildCPUTip, ram: buildRAMTip, engine: buildEngineTip,
+ vram: buildVRAMTip, rsv: buildRSVTip, pwr: buildPWRTip };
+ const html = builders[key]?.(snap, en());
+ if (html) showTooltip(el, html);
+}
+
+function buildCPUTip(snap, eng) {
+ const pct = snap.cpu_pct ?? 0;
+ const freq = snap.cpu_freq_ghz ?? 0;
+ const c = pct > 80 ? "#ff4d4f" : pct > 50 ? "#faad14" : "#52c41a";
+ if (eng) {
+ return tipTitle("🖥️ CPU")
+ + tipRow("Utilisation", pct.toFixed(1) + " %", c)
+ + (snap.cpu_model ? tipRow("Model", snap.cpu_model) : "")
+ + (freq > 0 ? tipRow("Freq", freq.toFixed(2) + " GHz") : "")
+ + (snap.cpu_threads ? tipRow("Threads", String(snap.cpu_threads)) : "")
+ + tipNote("Source: psutil · cpu_percent / wmic CurrentClockSpeed\nRegistry: ProcessorNameString");
+ }
+ return tipTitle("🖥️ 处理器")
+ + tipRow("占用率", pct.toFixed(1) + " %", c)
+ + (snap.cpu_model ? tipRow("型号", snap.cpu_model) : "")
+ + (freq > 0 ? tipRow("频率", freq.toFixed(2) + " GHz") : "")
+ + (snap.cpu_threads ? tipRow("线程数", String(snap.cpu_threads)) : "")
+ + tipNote("来源: psutil · cpu_percent / wmic CurrentClockSpeed\n注册表: ProcessorNameString");
+}
+
+function buildRAMTip(snap, eng) {
+ const pct = snap.ram_pct ?? 0;
+ const total = snap.ram_total_gb ?? 0;
+ const used = snap.ram_used_gb ?? 0;
+ const free = snap.ram_free_gb ?? 0;
+ const commitUsed = snap.commit_used_gb ?? 0;
+ const commitLimit = snap.commit_limit_gb ?? 0;
+ const c = pct > 90 ? "#ff4d4f" : pct > 70 ? "#faad14" : "#52c41a";
+ const commitStr = commitLimit > 0
+ ? `${commitUsed.toFixed(1)} / ${commitLimit.toFixed(1)} GB`
+ : `${commitUsed.toFixed(1)} GB`;
+ if (eng) {
+ return tipTitle("💾 System RAM")
+ + tipRow("Total", total.toFixed(1) + " GB")
+ + tipRow("Used", used.toFixed(1) + " GB", c)
+ + tipRow("Free", free.toFixed(1) + " GB", "#52c41a")
+ + (commitUsed > 0 ? tipRow("Commit", commitStr, "#b37feb") : "")
+ + tipNote("Source: psutil · virtual_memory\nCommit: GlobalMemoryStatusEx");
+ }
+ return tipTitle("💾 系统内存")
+ + tipRow("总量", total.toFixed(1) + " GB")
+ + tipRow("已用", used.toFixed(1) + " GB", c)
+ + tipRow("空闲", free.toFixed(1) + " GB", "#52c41a")
+ + (commitUsed > 0 ? tipRow("虚拟内存", commitStr, "#b37feb") : "")
+ + tipNote("来源: psutil · virtual_memory\n虚拟内存: GlobalMemoryStatusEx");
+}
+
+function buildEngineTip(snap, eng) {
+ const load = snap.gpu_load_pct ?? 0;
+ const freq = snap.gpu_freq_mhz ?? 0;
+ const temp = snap.gpu_temp_c ?? -1;
+ const c = load > 95 ? "#ff4d4f" : load > 80 ? "#faad14" : "#52c41a";
+ const tc = temp > 85 ? "#ff4d4f" : temp > 70 ? "#faad14" : "#36cfc9";
+ if (eng) {
+ return tipTitle("📊 GPU Engine")
+ + tipRow("Load", load.toFixed(1) + " %", c)
+ + (freq > 0 ? tipRow("Clock", Math.round(freq) + " MHz") : "")
+ + (temp >= 0 ? tipRow("Temp", Math.round(temp) + " °C", tc) : "")
+ + tipNote("Source: Intel Level Zero · zesEngineGetActivity\n" +
+ " zesFrequencyGetState · zesTemperatureGetState");
+ }
+ return tipTitle("📊 GPU 引擎")
+ + tipRow("负载", load.toFixed(1) + " %", c)
+ + (freq > 0 ? tipRow("频率", Math.round(freq) + " MHz") : "")
+ + (temp >= 0 ? tipRow("温度", Math.round(temp) + " °C", tc) : "")
+ + tipNote("来源: Intel Level Zero · zesEngineGetActivity\n" +
+ " zesFrequencyGetState · zesTemperatureGetState");
+}
+
+function buildVRAMTip(snap, eng) {
+ const total = snap.vram_total_gb ?? 0;
+ const used = snap.vram_driver_used_gb ?? 0;
+ const alloc = snap.vram_allocated_gb ?? 0;
+ const rsv = snap.vram_reserved_gb ?? 0;
+ const free = snap.vram_free_gb ?? 0;
+ // Breakdown: C = driver_used - pytorch_reserved, E = reserved - allocated
+ const sysEnv = Math.max(0, used - rsv);
+ const buf = Math.max(0, rsv - alloc);
+ const pct = total > 0 ? used / total : 0;
+ const c = pct > 0.95 ? "#ff4d4f" : pct > 0.85 ? "#ff7a00" : "#e8e8e8";
+ // 统一用 GB(2位小数)显示,与 bar 上的 toFixed(1) 对齐,避免 MB÷1000 的心算误差
+ const g2 = gb => gb.toFixed(2) + " GB";
+ if (eng) {
+ return tipTitle("🧠 VRAM Breakdown")
+ + tipRow("Total", g2(total))
+ + tipRow("Current Used", g2(used), c)
+ + tipRow(" System & Env (display & driver)", g2(sysEnv), "#888")
+ + tipRow(" Model & Compute (loaded model)", g2(alloc), "#36cfc9")
+ + tipRow(" Reserved Buffer (PyTorch pre-alloc)", g2(buf), "#b37feb")
+ + tipRow("Free", g2(free), "#52c41a")
+ + tipNote("Source: zesMemoryGetState / torch.xpu.memory_allocated / memory_reserved");
+ }
+ return tipTitle("🧠 显存详情")
+ + tipRow("总量", g2(total))
+ + tipRow("当前占用", g2(used), c)
+ + tipRow(" 系统与环境 (系统显示及驱动占用)", g2(sysEnv), "#888")
+ + tipRow(" 模型与计算 (当前加载模型与实时运算)", g2(alloc), "#36cfc9")
+ + tipRow(" 预留缓冲区 (PyTorch 预先霸占的待分配空间)", g2(buf), "#b37feb")
+ + tipRow("空闲", g2(free), "#52c41a")
+ + tipNote("来源: zesMemoryGetState / torch.xpu.memory_allocated / memory_reserved");
+}
+
+function buildRSVTip(snap, eng) {
+ const rsv = snap.vram_reserved_gb ?? 0;
+ const alloc = snap.vram_allocated_gb ?? 0;
+ const buf = Math.max(0, rsv - alloc);
+ if (eng) {
+ return tipTitle("💾 Reserved (PyTorch Cache)")
+ + tipRow("Cache Pool", (rsv * 1024).toFixed(0) + " MB", "#b37feb")
+ + tipRow(" In Use", (alloc * 1024).toFixed(0) + " MB", "#36cfc9")
+ + tipRow(" Free Buf", (buf * 1024).toFixed(0) + " MB", "#888")
+ + tipNote("Source: torch.xpu.memory_reserved()");
+ }
+ return tipTitle("💾 PyTorch 缓存池")
+ + tipRow("缓存总量", (rsv * 1024).toFixed(0) + " MB", "#b37feb")
+ + tipRow(" 实际占用", (alloc * 1024).toFixed(0) + " MB", "#36cfc9")
+ + tipRow(" 空闲缓存", (buf * 1024).toFixed(0) + " MB", "#888")
+ + tipNote("来源: torch.xpu.memory_reserved()");
+}
+
+function buildPWRTip(snap, eng) {
+ if (!snap.power_available) {
+ const dev = shortDeviceName(snap.device_name);
+ if (eng) {
+ return tipTitle("⚡ Power — 🔒 Admin Only")
+ + `${dev} power data requires admin privileges. ` +
+ `Run ComfyUI as Administrator to enable live power monitoring.
`;
+ }
+ return tipTitle("⚡ 功率 — 🔒 需要管理员")
+ + `${dev} 功率数据需要管理员权限。 ` +
+ `以管理员身份运行 ComfyUI 即可启用实时功率监控。
`;
+ }
+ const tgp = resolveTGP(snap);
+ const dev = shortDeviceName(snap.device_name);
+ const pct = tgp > 0 ? snap.power_w / tgp : 0;
+ const c = (tgp > 0 && pct > 0.95) ? "#ff4d4f"
+ : (tgp > 0 && pct > 0.80) ? "#b37feb"
+ : snap.power_w > 170 ? "#ff4d4f"
+ : snap.power_w > 120 ? "#b37feb"
+ : "#36cfc9";
+ if (eng) {
+ let html = tipTitle(`⚡ ${dev} Power`)
+ + tipRow("Instant Power", snap.power_w.toFixed(1) + " W", c);
+ if (tgp > 0) html += tipRow("TGP Limit", tgp.toFixed(0) + " W", "#666")
+ + tipRow("Load Ratio", (pct * 100).toFixed(0) + " %", c);
+ return html + tipNote("Source: Intel Level Zero · zesPowerGetEnergyCounter\nAdmin · Dual-sample energy delta");
+ }
+ let html = tipTitle(`⚡ ${dev} 实时功率`)
+ + tipRow("瞬时功率", snap.power_w.toFixed(1) + " W", c);
+ if (tgp > 0) html += tipRow("TGP 上限", tgp.toFixed(0) + " W", "#666")
+ + tipRow("负载比例", (pct * 100).toFixed(0) + " %", c);
+ return html + tipNote("来源: Intel Level Zero · zesPowerGetEnergyCounter\n需要管理员权限 · 双采样能量差值");
+}
+
+function buildPredictorTip(snap, eng) {
+ const total = _predModels.reduce((s, m) => s + (m.size || 0), 0);
+ const mPeak = _predModels.length > 0 ? Math.max(..._predModels.map(m => m.size || 0)) : 0;
+ const pred = calcPrediction(total, mPeak, snap);
+ const divider = `
`;
+
+ // ── 标题 ──
+ let html = tipTitle(eng ? "🎯 Current Load List" : "🎯 当前加载清单");
+
+ // ── 成功率输入参数 ──
+ const vendorName = pred.gpuVendor === "nvidia" ? "NVIDIA UVM" : "Intel Arc";
+ const overflowTol = pred.gamma.toFixed(1) + "x";
+ if (eng) {
+ html += tipRow("Overflow Tolerance", overflowTol + " (" + vendorName + ")", "#36cfc9");
+ html += tipRow("Eff. VRAM Cap", pred.vEff.toFixed(2) + " GB", "#36cfc9");
+ html += tipRow("Peak Model", mPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#ff4d4f" : "#52c41a");
+ html += tipRow(" VRAM Gap", pred.dPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#faad14" : "#555");
+ html += tipRow(" P_peak", (pred.pPeak * 100).toFixed(0) + " %", pred.dPeak > 0 ? "#faad14" : "#52c41a");
+ html += tipRow("Total Models", total.toFixed(2) + " GB", pred.color);
+ html += tipRow(" Load Gap", pred.dLoad.toFixed(2) + " GB", pred.dLoad > 0 ? "#faad14" : "#555");
+ html += tipRow(" Avail. RAM", pred.cRam.toFixed(2) + " GB", "#b37feb");
+ html += tipRow(" Avail. Commit", pred.sVirt.toFixed(2) + " GB", "#888");
+ html += tipRow(" P_load", (pred.pLoad * 100).toFixed(0) + " %", pred.dLoad > 0 ? "#faad14" : "#52c41a");
+ html += tipRow("Success Rate", pred.rate + " %", pred.color);
+ } else {
+ html += tipRow("溢出容忍", overflowTol + " (" + vendorName + ")", "#36cfc9");
+ html += tipRow("显存上限", pred.vEff.toFixed(2) + " GB", "#36cfc9");
+ html += tipRow("峰值模型", mPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#ff4d4f" : "#52c41a");
+ html += tipRow(" 显存缺口", pred.dPeak.toFixed(2) + " GB", pred.dPeak > 0 ? "#faad14" : "#555");
+ html += tipRow(" 显存压力", (pred.pPeak * 100).toFixed(0) + " %", pred.dPeak > 0 ? "#faad14" : "#52c41a");
+ html += tipRow("模型总量", total.toFixed(2) + " GB", pred.color);
+ html += tipRow(" 负载缺口", pred.dLoad.toFixed(2) + " GB", pred.dLoad > 0 ? "#faad14" : "#555");
+ html += tipRow(" 可用内存", pred.cRam.toFixed(2) + " GB", "#b37feb");
+ html += tipRow(" 可用虚拟内存", pred.sVirt.toFixed(2) + " GB", "#888");
+ html += tipRow(" 负载压力", (pred.pLoad * 100).toFixed(0) + " %", pred.dLoad > 0 ? "#faad14" : "#52c41a");
+ html += tipRow("预测成功率", pred.rate + " %", pred.color);
+ }
+
+ // ── 分隔线 + 模型列表 ──
+ html += divider;
+ const sorted = [..._predModels].sort((a, b) => b.size - a.size);
+ if (sorted.length === 0) {
+ html += `${eng ? "No active models detected." : "无活跃模型"}
`;
+ } else {
+ for (const m of sorted) {
+ const short = m.name.split(/[\\/]/).pop();
+ html += tipRow(short, m.size.toFixed(2) + " GB", "#52c41a");
+ }
+ }
+
+ html += tipNote(eng ? "Source: disk file size · /xpusys/model_sizes"
+ : "来源: 磁盘文件大小 · /xpusys/model_sizes");
+ return html;
+}
+
+// 给节点的模型 widget 挂 callback 钩子,切换模型时立即触发更新
+const MODEL_EXTS = [".safetensors", ".ckpt", ".pt", ".pth", ".bin", ".gguf", ".sft", ".pkl"];
+
+function applyModelHook(node) {
+ let hasModel = false;
+ node.widgets?.forEach(w => {
+ if (w.type !== "combo" || w._xpusysPredHooked) return;
+ const wn = w.name?.toLowerCase() || "";
+ const isModel =
+ ["model","ckpt","vae","lora","control","clip","unet"].some(k => wn.includes(k)) ||
+ w.options?.values?.some(v => {
+ const s = String(v).toLowerCase();
+ return MODEL_EXTS.some(ext => s.endsWith(ext));
+ });
+ if (!isModel) return;
+ hasModel = true;
+ const origCb = w.callback;
+ w.callback = function () {
+ const r = origCb ? origCb.apply(this, arguments) : undefined;
+ updatePredictor();
+ return r;
+ };
+ w._xpusysPredHooked = true;
+ });
+
+ // 只对含模型 widget 的节点挂 bypass / 删除钩子
+ if (hasModel && !node._xpusysNodeHooked) {
+ // 监听节点被移除
+ const origRemoved = node.onRemoved;
+ node.onRemoved = function () {
+ if (origRemoved) origRemoved.apply(this, arguments);
+ updatePredictor();
+ };
+ // 使用 Object.defineProperty 监听 mode 属性变化
+ let _mode = node.mode;
+ Object.defineProperty(node, "mode", {
+ get: function() { return _mode; },
+ set: function(v) {
+ if (_mode !== v) {
+ _mode = v;
+ updatePredictor();
+ }
+ },
+ configurable: true
+ });
+ node._xpusysNodeHooked = true;
+ }
+}
+
+// Debounced entry point — called by graph event hooks
+function updatePredictor() {
+ if (_predTimer) clearTimeout(_predTimer);
+ _predTimer = setTimeout(_doPredictorFetch, 150);
+}
+
+async function _doPredictorFetch() {
+ const nodes = app.graph?._nodes;
+ if (!nodes) return;
+
+ // 后端支持的模型文件后缀
+ const ALLOWED_EXTS = [".safetensors", ".gguf", ".ckpt", ".pt", ".pth", ".bin", ".onnx", ".pkl"];
+
+ // 按模型文件名去重的 Map
+ const uniqueModels = new Map();
+
+ nodes.forEach(node => {
+ if (node.mode !== 0) return; // skip bypassed / muted
+ const nodeType = node.type?.toLowerCase() || "";
+
+ // 遍历所有 widget,查找符合后缀的模型文件
+ node.widgets?.forEach(w => {
+ if (typeof w.value !== "string") return;
+ const value = w.value.trim();
+ if (!value) return;
+
+ // 检查是否为已登记后缀的模型文件
+ const ext = value.substring(value.lastIndexOf(".")).toLowerCase();
+ if (!ALLOWED_EXTS.includes(ext)) return;
+
+ // 按文件名去重,只保留第一次出现的记录
+ if (!uniqueModels.has(value)) {
+ // value 可能包含子文件夹路径,如 "子文件夹/model.safetensors"
+ // 分离路径和文件名:path 用于查找,name 用于显示
+ const lastSlash = value.lastIndexOf("/");
+ const lastBackslash = value.lastIndexOf("\\");
+ const sepIndex = Math.max(lastSlash, lastBackslash);
+ const modelPath = value; // 完整相对路径,用于后端查找
+ const modelName = sepIndex >= 0 ? value.substring(sepIndex + 1) : value; // 纯文件名,用于显示
+
+ uniqueModels.set(value, { path: modelPath, name: modelName });
+ }
+ });
+ });
+
+ const activeModels = Array.from(uniqueModels.values());
+
+ try {
+ const r = await api.fetchApi("/xpusys/model_sizes", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ models: activeModels }),
+ });
+ if (r.ok) {
+ const data = await r.json();
+ _predModels = data.models || [];
+ renderPredictor();
+ }
+ } catch (_) {}
+}
+
+// ---------------------------------------------------------------------------
+// Data
+// ---------------------------------------------------------------------------
+
+let _pollTimer = null;
+
+function onWsMessage(e) {
+ if (e?.detail?.type !== "xpusys_stats") return;
+ renderSnap(e.detail.data);
+}
+
+async function pollOnce() {
+ try {
+ const r = await api.fetchApi("/xpusys/stats");
+ if (r.ok) renderSnap(await r.json());
+ } catch (_) {}
+}
+
+function startPolling() {
+ if (_pollTimer) clearInterval(_pollTimer);
+ const ms = Math.max(200, getSetting(S.refreshMs, 1000));
+ _pollTimer = setInterval(pollOnce, ms);
+ pollOnce();
+}
+
+// ---------------------------------------------------------------------------
+// Extension
+// ---------------------------------------------------------------------------
+
+app.registerExtension({
+ name: `${NS}.Monitor`,
+
+ async setup() {
+ injectStyles();
+ createTooltip();
+
+ // ── 0 关于 ────────────────────────────────────────────────────────────
+ app.ui.settings.addSetting({
+ id: `${NS}.About`,
+ name: t("关于插件", "About"),
+ type: (_name, _setter, _value) => {
+ const wrap = document.createElement("div");
+ wrap.style.cssText =
+ "line-height:1.7;color:#ccc;font-size:15px;padding:4px 0 2px;max-width:520px;";
+ const zhText =
+ `本插件源于对 Intel Arc (XPU) 生态的支持。虽由「少数派」发起,但遵循底层标准,现已实现对 Intel (XPU) 与 NVIDIA (CUDA) 的完美兼容(AMD 支持已在计划中)。 ` +
+ `核心亮点: ` +
+ `独家支持模型运行显存预测,在生成前预判硬件压力;提供精准的跨平台硬件监测。填补工具空白,追求极致稳定。希望你喜欢。`;
+ const enText =
+ `Born from the Intel Arc (XPU) ecosystem. While built by the "minority," this plugin follows standard specs for seamless Intel (XPU) and NVIDIA (CUDA) support (AMD in progress). ` +
+ `Highlight: ` +
+ `Exclusive Model VRAM Prediction to anticipate hardware strain before generation. We aim to fill the gap in XPU monitoring with stable, cross-platform insights. Enjoy!`;
+ wrap.innerHTML = en() ? enText : zhText;
+
+ // ── 版本号 + GitHub 按钮 ──────────────────────────────────────────
+ const bar = document.createElement("div");
+ bar.style.cssText =
+ "display:flex;align-items:center;justify-content:flex-end;gap:8px;" +
+ "margin-top:16px;flex-wrap:wrap;border-top:1px solid #333;padding-top:10px;";
+
+ // 版本徽章
+ const verBadge = document.createElement("span");
+ verBadge.style.cssText =
+ "display:inline-flex;align-items:center;gap:0;border-radius:4px;overflow:hidden;" +
+ "font-size:12px;font-weight:600;line-height:1;";
+ verBadge.innerHTML =
+ `${t("版本", "Version")} ` +
+ `${VERSION} `;
+
+ // GitHub 按钮
+ const ghBtn = document.createElement("a");
+ ghBtn.href = GITHUB;
+ ghBtn.target = "_blank";
+ ghBtn.rel = "noopener noreferrer";
+ ghBtn.style.cssText =
+ "display:inline-flex;align-items:center;gap:5px;padding:4px 10px;" +
+ "background:#24292e;color:#fff;border-radius:4px;font-size:12px;font-weight:600;" +
+ "text-decoration:none;line-height:1;transition:background .15s;";
+ ghBtn.onmouseenter = () => { ghBtn.style.background = "#444d56"; };
+ ghBtn.onmouseleave = () => { ghBtn.style.background = "#24292e"; };
+ ghBtn.innerHTML =
+ `` +
+ ` ` +
+ `GitHub`;
+
+ bar.appendChild(verBadge);
+ bar.appendChild(ghBtn);
+ wrap.appendChild(bar);
+
+ return wrap;
+ },
+ defaultValue: "",
+ category: [NS, t("\uE000关于", "\uE000About"), t("\uE000简介", "\uE000Introduction")],
+ });
+
+ // ── 1 通用设置 ────────────────────────────────────────────────────────
+ app.ui.settings.addSetting({
+ id: S.lang, name: t("界面语言", "Interface Language"),
+ tooltip: t("切换悬浮窗与状态栏的显示语言", "Switch display language for overlay and status bar"),
+ type: makeLangSelectType(), defaultValue: "system",
+ category: [NS, t("\uE001通用设置", "\uE001General"), t("\uE003语言", "\uE003Language")],
+ });
+
+ app.ui.settings.addSetting({
+ id: S.fontSize, name: t("字体大小 (px)", "Font Size (px)"),
+ tooltip: t("状态栏字体大小,范围 12–22 px", "Status bar font size, range 12–22 px"),
+ type: makeSliderType(12, 22, 1, false), defaultValue: 16,
+ category: [NS, t("\uE001通用设置", "\uE001General"), t("\uE002字体大小", "\uE002Font Size")],
+ onChange: applyFontSize,
+ });
+ app.ui.settings.addSetting({
+ id: S.refreshMs, name: t("刷新间隔 (ms)", "Refresh Interval (ms)"),
+ tooltip: t("状态栏数据更新频率,范围 200–5000 ms", "Status bar update frequency, range 200–5000 ms"),
+ type: makeSliderType(200, 5000, 100), defaultValue: 1000,
+ category: [NS, t("\uE001通用设置", "\uE001General"), t("\uE001刷新间隔", "\uE001Refresh Interval")],
+ onChange: startPolling,
+ });
+
+ // ── 2 工作流预测 ──────────────────────────────────────────────────────
+ app.ui.settings.addSetting({
+ id: S.showPredictor, name: t("显示显存预测(PRED)", "Show VRAM Predictor (PRED)"),
+ tooltip: t("在状态栏最左侧显示工作流模型显存预估值与成功率", "Show estimated VRAM usage and workflow success rate in the status bar"),
+ type: "boolean", defaultValue: true,
+ category: [NS, t("\uE002工作流预测", "\uE002Workflow Predictor"), t("\uE001显示显存预测", "\uE001Show VRAM Predictor")],
+ onChange: applyVisibility,
+ });
+
+ // ── 3 CPU监控 ─────────────────────────────────────────────────────────
+ app.ui.settings.addSetting({
+ id: S.showCPU, name: t("显示 CPU 负载", "Show CPU Load"),
+ tooltip: t("在状态栏显示 CPU 占用率与频率", "Show CPU usage and frequency in the status bar"),
+ type: "boolean", defaultValue: false,
+ category: [NS, t("\uE003CPU监控", "\uE003CPU Monitor"), t("\uE001显示CPU负载", "\uE001Show CPU Load")],
+ onChange: applyVisibility,
+ });
+
+ // ── 4 内存监控 ────────────────────────────────────────────────────────
+ app.ui.settings.addSetting({
+ id: S.showRAM, name: t("显示内存占用", "Show RAM Usage"),
+ tooltip: t("在状态栏显示系统内存使用率", "Show system RAM usage in the status bar"),
+ type: "boolean", defaultValue: true,
+ category: [NS, t("\uE004内存监控", "\uE004RAM Monitor"), t("\uE001显示内存占用", "\uE001Show RAM Usage")],
+ onChange: applyVisibility,
+ });
+
+ // ── 5 显卡监控 ────────────────────────────────────────────────────────
+ app.ui.settings.addSetting({
+ id: S.showEngine, name: t("显示 GPU 引擎(负载 / 频率 / 温度)", "Show GPU Engine (Load / Freq / Temp)"),
+ tooltip: t("在状态栏显示 GPU 负载百分比、时钟频率和核心温度", "Show GPU load, clock frequency and core temperature in the status bar"),
+ type: "boolean", defaultValue: true,
+ category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE001GPU引擎", "\uE001GPU Engine")],
+ onChange: applyVisibility,
+ });
+ app.ui.settings.addSetting({
+ id: S.showVRAM, name: t("显示显存用量", "Show VRAM Usage"),
+ tooltip: t("在状态栏显示驱动层显存占用", "Show driver-level VRAM usage in the status bar"),
+ type: "boolean", defaultValue: true,
+ category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE002显存用量", "\uE002VRAM Usage")],
+ onChange: applyVisibility,
+ });
+ app.ui.settings.addSetting({
+ id: S.showRSV, name: t("显示 PyTorch 缓存池(RSV)", "Show PyTorch Cache Pool (RSV)"),
+ tooltip: t("在状态栏显示 torch.xpu.memory_reserved() 缓存大小", "Show torch.xpu.memory_reserved() cache size in the status bar"),
+ type: "boolean", defaultValue: false,
+ category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE003缓存池", "\uE003Cache Pool")],
+ onChange: applyVisibility,
+ });
+ app.ui.settings.addSetting({
+ id: S.showPower, name: t("显示功率(需要管理员权限)", "Show Power (Admin Required)"),
+ tooltip: t("在状态栏显示瞬时功耗与 TGP 负载比例", "Show instantaneous power consumption and TGP load ratio in the status bar"),
+ type: "boolean", defaultValue: true,
+ category: [NS, t("\uE005显卡监控", "\uE005GPU Monitor"), t("\uE004功率", "\uE004Power")],
+ onChange: applyVisibility,
+ });
+
+ const bar = buildBar();
+ mountBar(bar);
+ applyVisibility();
+ applyFontSize();
+ setTimeout(applyFontSize, 0);
+
+ api.addEventListener("message", onWsMessage);
+ startPolling();
+
+ // 对已存在节点补挂 widget 钩子,然后初始扫描
+ app.graph._nodes?.forEach(n => applyModelHook(n));
+ updatePredictor();
+ },
+
+ // ── 显存预测 — 官方扩展钩子,安全无冲突 ──────────────────────────────
+ nodeCreated(node) {
+ setTimeout(() => { applyModelHook(node); updatePredictor(); }, 200);
+ },
+
+ loadedGraphNode(node) {
+ applyModelHook(node);
+ },
+
+ async afterConfigureGraph() {
+ // 对所有节点应用 hook,然后更新预测
+ app.graph._nodes?.forEach(n => applyModelHook(n));
+ updatePredictor();
+ },
+});