/** * 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(); }, });