Files
kk8bit-KayTool/web/resourcemonitor.js
T
kk8bit 080eaeec52 Fix queue parameter loss, Monitor button state and pilgram fallback
The queuePrompt override forwarded only its first two arguments, but the
frontend passes partialExecutionTargets and previewMethod in a third one.
Dropping it made ComfyUI's own partial execution run the entire workflow
and disabled the preview method chosen in the UI. Forward arguments as
given. Closes #30, #31, #32.

The Monitor button applied its enabled setting only from the setting's
onChange handler, so a page load always appended it visible regardless of
the stored value. Apply the setting when the button is created. Closes
#19.

A missing pilgram shelled out to `pip install`, which raises in any
environment without a pip module (uv-created virtualenvs, among others)
and takes the whole package's import down with it. Degrade to an empty
filter list and a warning instead.

Also declare pilgram and requests in pyproject.toml, which registry
installs read instead of requirements.txt.
2026-09-20 18:08:50 +08:00

882 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../scripts/app.js";
import { api } from "../../scripts/api.js";
class KayPromptExecution {
constructor(id) {
this.promptApi = null;
this.executedNodeIds = [];
this.totalNodes = 0;
this.currentlyExecuting = null;
this.errorDetails = null;
this.id = id;
}
setPrompt(prompt) {
this.promptApi = prompt.output;
this.totalNodes = Object.keys(this.promptApi).length;
}
getApiNode(nodeId) {
return this.promptApi?.[String(nodeId)] || null;
}
getNodeLabel(nodeId) {
const apiNode = this.getApiNode(nodeId);
let label = apiNode?._meta?.title || apiNode?.class_type || undefined;
if (!label) {
const graphNode = window?.app?.graph?.getNodeById(Number(nodeId));
label = graphNode?.title || graphNode?.type || undefined;
}
return label || "Unknown";
}
executing(nodeId, step, maxSteps) {
if (nodeId == null) {
this.currentlyExecuting = null;
return;
}
if (this.currentlyExecuting?.nodeId !== nodeId) {
if (this.currentlyExecuting != null && !this.executedNodeIds.includes(this.currentlyExecuting.nodeId)) {
this.executedNodeIds.push(this.currentlyExecuting.nodeId);
}
this.currentlyExecuting = { nodeId, nodeLabel: this.getNodeLabel(nodeId), pass: 0 };
const apiNode = this.getApiNode(nodeId);
if (apiNode?.class_type === "UltimateSDUpscale") {
this.currentlyExecuting.pass--;
this.currentlyExecuting.maxPasses = -1;
} else if (apiNode?.class_type === "IterativeImageUpscale") {
this.currentlyExecuting.maxPasses = apiNode?.inputs["steps"] ?? -1;
}
}
if (step != null && maxSteps != null) {
if (!this.currentlyExecuting.step || step < this.currentlyExecuting.step) {
this.currentlyExecuting.pass++;
}
this.currentlyExecuting.step = step;
this.currentlyExecuting.maxSteps = maxSteps;
}
}
error(details) {
this.errorDetails = details;
}
}
const KayResourceMonitor = {
toolbar: null,
chartCanvas: null,
dataDisplay: null,
menuButton: null,
workflowProgressEl: null,
dataHistory: { cpu: [], ram: [], gpu: [], gpuMem: [], gpuTemp: [] },
displayHistory: { cpu: [], ram: [], gpu: [], gpuMem: [], gpuTemp: [] },
current: {
cpu: 0, ram: 0, gpu: 0, gpuMem: 0, gpuTemp: 0,
ramUsed: 0, ramTotal: 0, gpuMemUsed: 0, gpuMemTotal: 0
},
target: {
cpu: 0, ram: 0, gpu: 0, gpuMem: 0, gpuTemp: 0,
ramTotal: 0, ramUsed: 0, gpuMemTotal: 0, gpuMemUsed: 0,
cpuName: "Unknown", gpuName: "Unknown"
},
workflowProgress: {
percentage: 0,
nodePercentage: 0,
currentNodePercentage: 0,
startTime: null,
totalDuration: null,
currentNode: "Idle",
currentNodeLabel: "Idle",
easeOutStartTime: null,
isInterrupted: false,
isProgressUnknown: false,
queueCount: 0,
completionTime: null,
resetAnimationStartTime: null,
},
currentWorkflow: { percentage: 0 },
promptsMap: new Map(),
currentExecution: null,
lastQueueRemaining: 0,
cpuCores: 4,
dragState: { isDragging: false, offsetX: 0, offsetY: 0 },
resizeState: { isResizing: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 },
position: { left: 0, top: 0 },
size: { width: 250, height: 219 },
lastUpdate: 0, lastCurveUpdate: 0, lastDisplayUpdate: 0,
maxPoints: 120,
curveInterval: 0,
displayInterval: 0,
smoothFactor: 0.1,
easeOutDuration: 500,
minWidth: 150,
minHeight: 90,
maxHeight: 219,
edgeThreshold: 10,
resizeHandleSize: 10,
colors: {
cpu: 'rgba(253, 170, 4, 0.86)',
ram: 'rgba(10, 124, 254, 0.86)',
gpu: 'rgba(255, 38, 0, 0.86)',
gpuMem: 'rgba(97, 208, 12, 0.86)',
gpuTemp: 'rgba(167, 43, 255, 0.86)',
header: 'rgba(128, 128, 128, 0.5)',
workflow: 'rgba(208, 255, 0, 0.86)',
},
styles: {
toolbar: `position: fixed; display: flex; flex-direction: column; color: rgba(186, 186, 186, 0.8); padding: 10px; border-radius: 5px; z-index: 10000; user-select: none; pointer-events: none; max-height: 219px;`,
header: `margin-bottom: 5px; font-weight: bold; font-size: 10px;`,
headerText: `cursor: grab; pointer-events: auto; display: inline-block;`,
canvas: `margin-top: 5px; margin-bottom: 10px; width: 100%; height: 100px; pointer-events: none;`,
display: `display: flex; flex-direction: column; font-size: 10px; flex-grow: 1; overflow: hidden; pointer-events: none;`,
resizeHandle: `position: absolute; bottom: 0; right: 0; width: 8px; height: 8px; border-bottom: 1px solid; border-right: 1px solid; cursor: se-resize; pointer-events: auto;`,
dot: (color) => `display: inline-block; width: 3px; height: 3px; border-radius: 50%; background: ${color}; margin-right: 5px; flex-shrink: 0; transition: background 0.5s ease;`,
bar: (width) => `display: inline-block; width: ${width}px; height: 3px; background: rgba(200, 200, 200, 0.3); margin-right: 5px; position: relative; flex-shrink: 0;`,
fill: (color, percent) => `position: absolute; top: 0; left: 0; width: ${percent}%; height: 100%; background: ${color};`,
row: `display: flex; align-items: center; height: 11px; line-height: 11px; white-space: nowrap;`,
text: `overflow: hidden; text-overflow: ellipsis; font-size: 10px;`
},
isVisible: true,
animationFrameId: null,
isEnabled: true,
isInitialized: false,
edgeFadeEnabled: true,
// 硬编码菜单边界
menuOffsets: {
left: 0, // 左侧菜单宽度
top: 0 // 顶部菜单高度
},
easeOutQuad(t) {
return t * (2 - t);
},
async init() {
if (this.isInitialized) return;
this.isInitialized = true;
this.currentWorkflow.percentage = 0;
this.workflowProgress.currentNodePercentage = 0;
this.isEnabled = app.ui.settings.getSettingValue("KayTool.EnableResourceMonitor");
this.edgeFadeEnabled = app.ui.settings.getSettingValue("KayTool.ResourceMonitorEdgeFade");
this.loadVisibility();
this.startMonitoring();
this.bindEvents();
this.setupUI();
this.setupWorkflowListener();
this.setupWebSocketListener();
if (this.isEnabled && this.isVisible) {
this.startAnimation();
}
},
async loadSettings() {
this.isEnabled = app.ui.settings.getSettingValue("KayTool.EnableResourceMonitor");
this.edgeFadeEnabled = app.ui.settings.getSettingValue("KayTool.ResourceMonitorEdgeFade");
},
loadVisibility() {
const visible = localStorage.getItem('KayResourceMonitorVisible');
this.isVisible = visible === null ? true : visible === 'true';
},
saveVisibility() {
localStorage.setItem('KayResourceMonitorVisible', this.isVisible.toString());
},
setupUI() {
this.toolbar = document.createElement('div');
this.toolbar.id = 'kay-resource-monitor';
const opacity = app.ui.settings.getSettingValue("KayTool.ResourceMonitorBackgroundOpacity") / 100;
Object.assign(this.toolbar.style, {
cssText: `${this.styles.toolbar} width: ${this.size.width}px; height: ${this.size.height}px; left: ${this.position.left}px; top: ${this.position.top}px; display: ${this.isVisible && this.isEnabled ? 'flex' : 'none'}; background: rgba(0, 0, 0, ${opacity});`
});
const header = document.createElement('div');
Object.assign(header.style, { cssText: this.styles.header });
const headerText = document.createElement('span');
Object.assign(headerText.style, { cssText: this.styles.headerText });
headerText.textContent = "𝙆 Resource Monitor";
header.appendChild(headerText);
this.toolbar.appendChild(header);
this.workflowProgressEl = document.createElement('div');
Object.assign(this.workflowProgressEl.style, { cssText: this.styles.row });
this.updateWorkflowProgress();
this.toolbar.appendChild(this.workflowProgressEl);
this.chartCanvas = document.createElement('canvas');
Object.assign(this.chartCanvas.style, { cssText: this.styles.canvas });
this.chartCanvas.width = this.size.width - 20;
this.chartCanvas.height = 100;
this.toolbar.appendChild(this.chartCanvas);
this.dataDisplay = document.createElement('div');
Object.assign(this.dataDisplay.style, { cssText: this.styles.display });
this.toolbar.appendChild(this.dataDisplay);
this.resizeHandle = document.createElement('div');
Object.assign(this.resizeHandle.style, { cssText: `${this.styles.resizeHandle} border-color: ${this.colors.header};` });
this.toolbar.appendChild(this.resizeHandle);
document.body.appendChild(this.toolbar);
this.loadPosition();
this.loadSize();
this.onResize();
this.bindToolbarEvents();
if (this.isEnabled && this.isVisible) {
this.startAnimation();
}
},
setupWebSocketListener() {
api.addEventListener("kaytool.resources", (event) => {
const data = event.detail;
this.updateTarget(data);
this.updateDisplay();
this.drawCurves();
});
},
setupWorkflowListener() {
const that = this;
const queuePrompt = api.queuePrompt;
api.queuePrompt = async function (num, prompt) {
let response;
try {
response = await queuePrompt.apply(api, [...arguments]);
} catch (e) {
const promptExecution = that.getOrMakePrompt("error");
promptExecution.error({ exception_type: "Unknown." });
throw e;
}
const promptExecution = that.getOrMakePrompt(response.prompt_id);
promptExecution.setPrompt(prompt);
that.promptsMap.set(response.prompt_id, promptExecution);
return response;
};
api.addEventListener("status", (e) => {
if (!e.detail?.exec_info) return;
this.lastQueueRemaining = e.detail.exec_info.queue_remaining;
this.dispatchProgressUpdate();
});
api.addEventListener("execution_start", (e) => {
const prompt = this.getOrMakePrompt(e.detail.prompt_id);
this.currentExecution = prompt;
this.workflowProgress.startTime = performance.now();
this.workflowProgress.totalDuration = null;
this.workflowProgress.percentage = 0;
this.workflowProgress.nodePercentage = 0;
this.workflowProgress.currentNodePercentage = 0;
this.currentWorkflow.percentage = 0;
this.workflowProgress.easeOutStartTime = null;
this.workflowProgress.isInterrupted = false;
this.workflowProgress.isProgressUnknown = false;
this.dispatchProgressUpdate();
});
api.addEventListener("executing", (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt("unknown");
}
this.currentExecution.executing(e.detail);
if (e.detail == null && !this.workflowProgress.isInterrupted) {
if (this.workflowProgress.startTime) {
this.workflowProgress.totalDuration = (performance.now() - this.workflowProgress.startTime) / 1000;
}
this.workflowProgress.easeOutStartTime = performance.now();
this.currentExecution = null;
}
this.dispatchProgressUpdate();
});
api.addEventListener("progress", (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
}
this.currentExecution.executing(e.detail.node, e.detail.value, e.detail.max);
this.dispatchProgressUpdate();
});
api.addEventListener("execution_cached", (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
}
for (const cached of e.detail.nodes) {
if (!this.currentExecution.executedNodeIds.includes(cached)) {
this.currentExecution.executedNodeIds.push(cached);
}
}
this.dispatchProgressUpdate();
});
api.addEventListener("execution_error", (e) => {
if (!this.currentExecution) {
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
}
this.currentExecution?.error(e.detail);
this.dispatchProgressUpdate();
});
api.addEventListener("execution_interrupted", (e) => {
if (this.currentExecution) {
if (this.workflowProgress.startTime) {
this.workflowProgress.totalDuration = (performance.now() - this.workflowProgress.startTime) / 1000;
}
this.workflowProgress.easeOutStartTime = performance.now();
this.workflowProgress.isInterrupted = true;
this.currentExecution = null;
this.dispatchProgressUpdate();
}
});
},
getOrMakePrompt(id) {
let prompt = this.promptsMap.get(id);
if (!prompt) {
prompt = new KayPromptExecution(id);
this.promptsMap.set(id, prompt);
}
return prompt;
},
dispatchProgressUpdate() {
const prompt = this.currentExecution;
this.workflowProgress.queueCount = this.lastQueueRemaining;
if (prompt?.currentlyExecuting) {
if (!prompt.promptApi || prompt.totalNodes <= 0) {
this.workflowProgress.isProgressUnknown = true;
this.workflowProgress.percentage = 0;
this.workflowProgress.currentNodeLabel = prompt.currentlyExecuting.nodeLabel || "Unknown";
} else {
this.workflowProgress.isProgressUnknown = false;
const executed = prompt.executedNodeIds.length;
const total = prompt.totalNodes || 1;
const basePercentage = (executed / total) * 100;
const current = prompt.currentlyExecuting;
let nodeContribution = 0;
this.workflowProgress.currentNodeLabel = current.nodeLabel || "Unknown";
if (current.step != null && current.maxSteps) {
this.workflowProgress.nodePercentage = (current.step / current.maxSteps) * 100;
nodeContribution = (1 / total) * (current.step / current.maxSteps) * 100;
} else {
this.workflowProgress.nodePercentage = 0;
}
this.workflowProgress.percentage = Math.min(basePercentage + nodeContribution, 100);
}
} else if (this.workflowProgress.totalDuration !== null) {
this.workflowProgress.isProgressUnknown = false;
if (this.workflowProgress.isInterrupted) {
this.workflowProgress.currentNode = `${this.workflowProgress.totalDuration.toFixed(1)}s (Interrupted)`;
this.workflowProgress.currentNodeLabel = "Interrupted";
} else {
this.workflowProgress.percentage = 100;
this.workflowProgress.currentNode = `${this.workflowProgress.totalDuration.toFixed(1)}s`;
this.workflowProgress.currentNodeLabel = "Completed";
}
this.workflowProgress.nodePercentage = 0;
this.workflowProgress.completionTime = performance.now(); // 新增:设置完成时间
} else {
this.workflowProgress.isProgressUnknown = false;
this.workflowProgress.percentage = 0;
this.workflowProgress.nodePercentage = 0;
this.workflowProgress.currentNode = "Idle";
this.workflowProgress.currentNodeLabel = "Idle";
this.workflowProgress.completionTime = null; // 新增:闲置时清空完成时间
}
},
updateWorkflowProgress() {
if (!this.workflowProgressEl) return;
const { percentage, currentNode, isProgressUnknown, queueCount } = this.workflowProgress;
const { styles, colors } = this;
const containerWidth = this.toolbar.clientWidth - 20;
const barWidth = Math.min(200, Math.max(50, (containerWidth - 15) * 0.18));
let displayPercentage, barPercentage, displayText;
if (currentNode === "Idle") {
displayText = "Workflow: Idle";
barPercentage = 0;
} else if (this.workflowProgress.resetAnimationStartTime !== null) {
// 重置动画期间,使用当前动画的百分比
displayPercentage = `${Math.min(Math.max(this.currentWorkflow.percentage, 0), 100).toFixed(1)}%`;
barPercentage = Math.min(Math.max(this.currentWorkflow.percentage, 0), 100);
displayText = `Workflow: ${displayPercentage} - ${currentNode}`;
} else {
displayPercentage = isProgressUnknown ? "Loading" : `${Math.min(Math.max(this.currentWorkflow.percentage, 0), 100).toFixed(1)}%`;
barPercentage = isProgressUnknown ? 0 : Math.min(Math.max(this.currentWorkflow.percentage, 0), 100);
if (queueCount > 1) {
displayPercentage += ` (${queueCount})`;
}
displayText = `Workflow: ${displayPercentage} - ${currentNode}`;
}
const dotColor = this.currentExecution?.currentlyExecuting ? 'rgba(208, 255, 0, 0.86)' : 'rgba(200, 200, 200, 0.3)';
if (!this.workflowProgressEl.querySelector('.workflow-dot')) {
this.workflowProgressEl.innerHTML = `
<span class="workflow-dot" style="${styles.dot(dotColor)}"></span>
<span class="workflow-bar" style="${styles.bar(barWidth)}">
<span style="${styles.fill(colors.workflow, barPercentage)}"></span>
</span>
<span style="${styles.text}">${displayText}</span>
`;
} else {
const dotElement = this.workflowProgressEl.querySelector('.workflow-dot');
if (dotElement) dotElement.style.background = dotColor;
const barElement = this.workflowProgressEl.querySelector('.workflow-bar');
if (barElement) {
barElement.style.width = `${barWidth}px`;
const barFill = barElement.querySelector('span');
if (barFill) barFill.style.width = `${barPercentage}%`;
}
const textElement = this.workflowProgressEl.querySelector('span:nth-child(3)');
if (textElement) textElement.textContent = displayText;
}
},
show() {
this.isVisible = true;
if (this.toolbar && this.isEnabled) {
this.toolbar.style.display = 'flex';
this.refreshCanvas();
this.startAnimation();
}
this.saveVisibility();
},
hide() {
this.isVisible = false;
if (this.toolbar) {
this.toolbar.style.display = 'none';
this.stopAnimation();
}
this.saveVisibility();
},
updateEnabledState(enabled) {
this.isEnabled = enabled;
if (this.menuButton) this.menuButton.style.display = enabled ? '' : 'none';
if (this.toolbar) {
this.toolbar.style.display = enabled && this.isVisible ? 'flex' : 'none';
if (enabled && this.isVisible) {
this.refreshCanvas();
this.startAnimation();
} else {
this.stopAnimation();
}
}
},
refreshCanvas() {
if (this.chartCanvas) {
const headerHeight = this.toolbar.firstChild.getBoundingClientRect().height + 10;
const workflowHeight = this.workflowProgressEl.clientHeight;
const dataHeight = this.dataDisplay.clientHeight;
this.chartCanvas.width = this.toolbar.clientWidth - 20;
this.chartCanvas.height = this.toolbar.clientHeight - headerHeight - workflowHeight - dataHeight - 10;
}
},
updateTarget(data) {
this.target.cpu = data.cpu_percent || 0;
this.cpuCores = data.cpu_count || 4;
this.target.ram = data.ram_percent || 0;
this.target.ramTotal = data.ram_total || 0;
this.target.ramUsed = data.ram_used || 0;
this.target.cpuName = data.cpu_name || "Unknown";
this.dataHistory.cpu.push(this.target.cpu);
this.dataHistory.ram.push(this.target.ram);
if (data.gpu && typeof data.gpu !== 'string' && data.gpu.length > 0) {
this.target.gpu = data.gpu[0].load || 0;
this.target.gpuMem = data.gpu[0].memory_percent || 0;
this.target.gpuMemTotal = data.gpu[0].memory_total || 0;
this.target.gpuMemUsed = data.gpu[0].memory_used || 0;
this.target.gpuTemp = data.gpu[0].temperature || 0;
this.target.gpuName = data.gpu[0].name || "Unknown";
this.dataHistory.gpu.push(this.target.gpu);
this.dataHistory.gpuMem.push(this.target.gpuMem);
this.dataHistory.gpuTemp.push(this.target.gpuTemp);
} else {
this.target.gpu = 0;
this.target.gpuMem = 0;
this.target.gpuMemTotal = 0;
this.target.gpuMemUsed = 0;
this.target.gpuTemp = 0;
this.target.gpuName = "Unknown";
this.dataHistory.gpu.push(0);
this.dataHistory.gpuMem.push(0);
this.dataHistory.gpuTemp.push(0);
}
if (this.dataHistory.cpu.length > this.maxPoints) {
this.dataHistory.cpu.shift();
this.dataHistory.ram.shift();
this.dataHistory.gpu.shift();
this.dataHistory.gpuMem.shift();
this.dataHistory.gpuTemp.shift();
}
},
updateDisplay() {
const { cpu, ram, gpu, gpuMem, gpuTemp, ramUsed, ramTotal, gpuMemUsed, gpuMemTotal } = this.current;
const { styles, colors } = this;
this.dataDisplay.innerHTML = [
this.renderRow('CPU', colors.cpu, cpu, `${cpu.toFixed(1)}% (${this.cpuCores} cores) - ${this.target.cpuName}`),
this.renderRow('RAM', colors.ram, ram, `${ramUsed.toFixed(1)}/${ramTotal.toFixed(1)}GB (${ram.toFixed(1)}%)`),
this.target.gpu || this.target.gpuMem
? [
this.renderRow('GPU', colors.gpu, gpu, `${gpu.toFixed(1)}% - ${this.target.gpuName}`),
this.renderRow('VRAM', colors.gpuMem, gpuMem, `${gpuMemUsed.toFixed(1)}/${gpuMemTotal.toFixed(1)}GB (${gpuMem.toFixed(1)}%)`),
this.renderRow('TEMP', colors.gpuTemp, gpuTemp, `${gpuTemp.toFixed(1)}°C (GPU)`)
].join('')
: [
this.renderRow('GPU', colors.gpu, 0, 'N/A'),
this.renderRow('VRAM', colors.gpuMem, 0, 'N/A'),
this.renderRow('TEMP', colors.gpuTemp, 0, 'N/A (GPU)')
].join('')
].join('');
},
renderRow(label, color, value, text) {
const { styles } = this;
const containerWidth = this.toolbar.clientWidth - 20;
const minBarWidth = 50;
const maxBarWidth = 200;
const fixedSpacing = 13;
const availableWidth = containerWidth - fixedSpacing;
const barWidth = Math.min(maxBarWidth, Math.max(minBarWidth, availableWidth * 0.18));
const textAvailableWidth = containerWidth - fixedSpacing - barWidth;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '10px sans-serif';
const textWidth = ctx.measureText(`${label}: ${text}`).width;
let displayText = `${label}: ${text}`;
if (textWidth > textAvailableWidth) {
const ellipsisWidth = ctx.measureText('...').width;
let truncatedText = `${label}: `;
let remainingWidth = textAvailableWidth - ctx.measureText(truncatedText).width - ellipsisWidth;
for (let i = 0; i < text.length; i++) {
const charWidth = ctx.measureText(text[i]).width;
if (remainingWidth < charWidth) {
truncatedText += '...';
break;
}
truncatedText += text[i];
remainingWidth -= charWidth;
}
displayText = truncatedText;
}
return `<div style="${styles.row}">
<span style="${styles.dot(color)}"></span>
<span style="${styles.bar(barWidth)}"><span style="${styles.fill(color, value)}"></span></span>
<span style="${styles.text}">${displayText}</span>
</div>`;
},
animate() {
if (!this.isVisible || !this.isEnabled || !this.toolbar) return;
const now = performance.now();
const deltaTime = (now - this.lastUpdate) / 1000;
this.lastUpdate = now;
for (const key in this.current) {
if (this.current[key] === 0 && this.target[key] !== 0) {
this.current[key] = this.target[key];
} else {
this.current[key] += (this.target[key] - this.current[key]) * Math.min(this.smoothFactor * deltaTime * 60, 1);
}
}
if (this.workflowProgress.easeOutStartTime !== null) {
const elapsed = now - this.workflowProgress.easeOutStartTime;
const t = Math.min(elapsed / this.easeOutDuration, 1);
const startPercentage = this.currentWorkflow.percentage;
const targetPercentage = this.workflowProgress.percentage;
const startNodePercentage = this.workflowProgress.currentNodePercentage;
const targetNodePercentage = this.workflowProgress.nodePercentage;
this.currentWorkflow.percentage = startPercentage + (targetPercentage - startPercentage) * this.easeOutQuad(t);
this.workflowProgress.currentNodePercentage = startNodePercentage + (targetNodePercentage - startNodePercentage) * this.easeOutQuad(t);
if (t >= 1) this.workflowProgress.easeOutStartTime = null;
} else if (!this.workflowProgress.isProgressUnknown && this.workflowProgress.currentNode !== "Idle" && this.workflowProgress.resetAnimationStartTime === null) {
this.currentWorkflow.percentage += (this.workflowProgress.percentage - this.currentWorkflow.percentage) * this.smoothFactor;
this.workflowProgress.currentNodePercentage += (this.workflowProgress.nodePercentage - this.workflowProgress.currentNodePercentage) * this.smoothFactor;
}
// 修改:处理 2 分钟后的重置动画
if (this.workflowProgress.completionTime !== null && this.workflowProgress.resetAnimationStartTime === null) {
const elapsedSinceCompletion = now - this.workflowProgress.completionTime;
if (elapsedSinceCompletion >= 120000) { // 2 分钟 = 120000 毫秒
this.workflowProgress.resetAnimationStartTime = now; // 开始重置动画
this.workflowProgress.percentage = this.currentWorkflow.percentage; // 锁定当前进度作为动画起点
this.workflowProgress.nodePercentage = this.workflowProgress.currentNodePercentage;
}
}
// 新增:执行重置动画
if (this.workflowProgress.resetAnimationStartTime !== null) {
const resetDuration = 1000; // 重置动画持续 1 秒
const elapsed = now - this.workflowProgress.resetAnimationStartTime;
const t = Math.min(elapsed / resetDuration, 1);
const easedT = this.easeOutQuad(t); // 使用现有的缓动函数
// 从当前值渐变到 0
this.currentWorkflow.percentage = this.workflowProgress.percentage * (1 - easedT);
this.workflowProgress.currentNodePercentage = this.workflowProgress.nodePercentage * (1 - easedT);
// 更新显示文本,保持平滑过渡
this.workflowProgress.currentNode = `Resetting... (${(this.currentWorkflow.percentage).toFixed(1)}%)`;
this.workflowProgress.currentNodeLabel = "Resetting";
if (t >= 1) {
// 动画结束,重置为 Idle
this.workflowProgress.percentage = 0;
this.workflowProgress.nodePercentage = 0;
this.workflowProgress.currentNodePercentage = 0;
this.workflowProgress.currentNode = "Idle";
this.workflowProgress.currentNodeLabel = "Idle";
this.workflowProgress.totalDuration = null;
this.workflowProgress.completionTime = null;
this.workflowProgress.resetAnimationStartTime = null;
this.currentWorkflow.percentage = 0;
}
}
if (this.currentExecution?.currentlyExecuting) {
if (this.workflowProgress.nodePercentage > 0) {
this.workflowProgress.currentNode = `${this.workflowProgress.currentNodeLabel} (${this.workflowProgress.currentNodePercentage.toFixed(1)}%)`;
} else {
this.workflowProgress.currentNode = this.workflowProgress.currentNodeLabel;
}
}
if (now - this.lastCurveUpdate >= 500) {
for (const key in this.displayHistory) {
this.displayHistory[key].push(this.current[key]);
if (this.displayHistory[key].length > this.maxPoints) this.displayHistory[key].shift();
}
this.lastCurveUpdate = now;
}
if (now - this.lastDisplayUpdate >= this.displayInterval) {
this.updateDisplay();
this.updateWorkflowProgress();
this.lastDisplayUpdate = now;
}
this.drawCurves();
this.animationFrameId = requestAnimationFrame(() => this.animate());
},
startAnimation() {
if (!this.animationFrameId && this.isEnabled && this.isVisible && this.toolbar) {
this.lastUpdate = performance.now();
this.lastCurveUpdate = performance.now();
this.lastDisplayUpdate = performance.now();
this.animationFrameId = requestAnimationFrame(() => this.animate());
}
},
stopAnimation() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
},
drawCurves() {
const ctx = this.chartCanvas.getContext('2d');
const w = this.chartCanvas.width;
const h = this.chartCanvas.height;
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = 'rgba(166, 166, 166, 0.5)';
ctx.lineWidth = 0.3;
ctx.beginPath();
for (let i = 0; i < 4; i++) {
const y = h - (i * h / 4);
ctx.moveTo(0, y);
ctx.lineTo(w, y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(166, 166, 166, 0.5)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(w, 0);
ctx.stroke();
const step = w / (this.maxPoints - 1);
const { colors } = this;
ctx.globalCompositeOperation = 'screen';
ctx.lineWidth = 0.8;
const now = performance.now();
const timeSinceLastUpdate = Math.min((now - this.lastCurveUpdate) / 500, 1);
[
{ data: this.displayHistory.cpu, color: colors.cpu, key: 'cpu' },
{ data: this.displayHistory.ram, color: colors.ram, key: 'ram' },
{ data: this.displayHistory.gpu, color: colors.gpu, key: 'gpu' },
{ data: this.displayHistory.gpuMem, color: colors.gpuMem, key: 'gpuMem' },
{ data: this.displayHistory.gpuTemp, color: colors.gpuTemp, key: 'gpuTemp' }
].forEach(({ data, color, key }) => {
ctx.strokeStyle = color;
ctx.beginPath();
const offset = step * timeSinceLastUpdate;
let firstVisiblePointDrawn = false;
for (let i = 0; i < data.length; i++) {
const x = i * step - offset;
const y = h - (Math.min(data[i], 100) / 100) * h;
if (x > w) continue;
if (i === 0 || (x < 0 && i === data.length - 1)) {
ctx.moveTo(Math.max(x, 0), y);
firstVisiblePointDrawn = x >= 0;
} else {
const prevX = (i - 1) * step - offset;
const prevY = h - (Math.min(data[i - 1], 100) / 100) * h;
if (prevX < 0 && x >= 0) {
const edgeX = 0;
const t = (edgeX - prevX) / (x - prevX);
const edgeY = prevY + (y - prevY) * t;
ctx.moveTo(edgeX, edgeY);
const midX = (edgeX + x) / 2;
ctx.quadraticCurveTo(edgeX, edgeY, midX, (edgeY + y) / 2);
ctx.quadraticCurveTo(x, y, x, y);
firstVisiblePointDrawn = true;
} else if (prevX >= 0 && x >= 0) {
const midX = (prevX + x) / 2;
ctx.quadraticCurveTo(prevX, prevY, midX, (prevY + y) / 2);
ctx.quadraticCurveTo(x, y, x, y);
firstVisiblePointDrawn = true;
} else if (prevX < 0 && x < 0 && !firstVisiblePointDrawn) {
ctx.moveTo(Math.max(x, 0), y);
}
}
}
const lastIndex = data.length - 1;
if (lastIndex >= 0) {
const lastX = lastIndex * step - offset;
const lastValue = data[lastIndex];
const currentValue = this.current[key];
const lastY = h - (Math.min(lastValue, 100) / 100) * h;
const nextX = Math.min(w, lastX + step * timeSinceLastUpdate);
const interpolatedValue = lastValue + (currentValue - lastValue) * timeSinceLastUpdate;
const nextY = h - (Math.min(interpolatedValue, 100) / 100) * h;
if (lastX >= 0 && nextX >= 0) {
ctx.quadraticCurveTo(lastX, lastY, (lastX + nextX) / 2, (lastY + nextY) / 2);
ctx.lineTo(nextX, nextY);
} else if (lastX < 0 && nextX >= 0) {
const edgeX = 0;
const t = (edgeX - lastX) / (nextX - lastX);
const edgeY = lastY + (nextY - lastY) * t;
ctx.moveTo(edgeX, edgeY);
ctx.quadraticCurveTo(edgeX, edgeY, (edgeX + nextX) / 2, (edgeY + nextY) / 2);
ctx.lineTo(nextX, nextY);
} else if (nextX < 0) {
ctx.moveTo(0, nextY);
}
}
ctx.stroke();
});
ctx.globalCompositeOperation = 'source-over';
if (this.edgeFadeEnabled) {
const gradient = ctx.createLinearGradient(0, 0, w, 0);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
gradient.addColorStop(0.1, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.9, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.save();
ctx.globalCompositeOperation = 'destination-in';
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
ctx.restore();
}
},
startMonitoring() {
for (const key in this.displayHistory) {
this.displayHistory[key] = Array(this.maxPoints).fill(0);
}
},
bindEvents() {
document.addEventListener('mousemove', (e) => {
if (this.dragState.isDragging) this.onDrag(e);
if (this.resizeState.isResizing) this.onResizeDrag(e);
});
document.addEventListener('mouseup', (e) => {
if (this.dragState.isDragging) this.endDrag();
if (this.resizeState.isResizing) this.endResize();
});
document.addEventListener('selectstart', (e) => (this.dragState.isDragging || this.resizeState.isResizing) && e.preventDefault());
window.addEventListener('resize', () => this.onResize());
},
bindToolbarEvents() {
if (this.toolbar) {
const headerText = this.toolbar.querySelector('span');
headerText.addEventListener('mousedown', (e) => this.startDrag(e));
this.resizeHandle.addEventListener('mousedown', (e) => this.startResize(e, 'se'));
}
},
loadPosition() {
const pos = JSON.parse(localStorage.getItem('KayResourceMonitorPosition')) || {};
const leftOffset = this.menuOffsets.left + 32; // 左侧菜单 + 20px
const topOffset = window.innerHeight - this.size.height - 20; // 底部 20px
this.position.left = pos.left != null ? pos.left : leftOffset;
this.position.top = pos.top != null ? pos.top : topOffset;
this.toolbar.style.left = `${this.position.left}px`;
this.toolbar.style.top = `${this.position.top}px`;
},
loadSize() {
const size = JSON.parse(localStorage.getItem('KayResourceMonitorSize')) || {};
this.size.width = Math.max(size.width || 250, this.minWidth);
this.size.height = Math.max(size.height || 219, this.minHeight);
this.toolbar.style.width = `${this.size.width}px`;
this.toolbar.style.height = `${this.size.height}px`;
},
savePositionAndSize() {
localStorage.setItem('KayResourceMonitorPosition', JSON.stringify(this.position));
localStorage.setItem('KayResourceMonitorSize', JSON.stringify(this.size));
},
onResize() {
if (!this.toolbar) return;
const winW = window.innerWidth;
const winH = window.innerHeight;
this.position.left = Math.max(this.menuOffsets.left, Math.min(this.position.left, winW - this.size.width));
this.position.top = Math.max(this.menuOffsets.top, Math.min(this.position.top, winH - this.size.height));
this.toolbar.style.left = `${this.position.left}px`;
this.toolbar.style.top = `${this.position.top}px`;
this.chartCanvas.width = this.toolbar.clientWidth - 20;
const headerHeight = this.toolbar.firstChild.getBoundingClientRect().height + 10;
const workflowHeight = this.workflowProgressEl.clientHeight;
const dataHeight = this.dataDisplay.clientHeight;
this.chartCanvas.height = this.toolbar.clientHeight - headerHeight - workflowHeight - dataHeight - 10;
this.updateDisplay();
},
startDrag(e) {
e.preventDefault();
const rect = this.toolbar.getBoundingClientRect();
this.dragState.isDragging = true;
this.dragState.offsetX = e.clientX - rect.left;
this.dragState.offsetY = e.clientY - rect.top;
this.toolbar.querySelector('span').style.cursor = 'grabbing';
},
onDrag(e) {
if (!this.dragState.isDragging) return;
const winW = window.innerWidth;
const winH = window.innerHeight;
this.position.left = Math.max(this.menuOffsets.left, Math.min(e.clientX - this.dragState.offsetX, winW - this.size.width));
this.position.top = Math.max(this.menuOffsets.top, Math.min(e.clientY - this.dragState.offsetY, winH - this.size.height));
this.toolbar.style.left = `${this.position.left}px`;
this.toolbar.style.top = `${this.position.top}px`;
},
endDrag() {
if (!this.dragState.isDragging) return;
this.dragState.isDragging = false;
this.toolbar.querySelector('span').style.cursor = 'grab';
this.savePositionAndSize();
},
startResize(e, direction) {
e.preventDefault();
this.resizeState.isResizing = true;
this.resizeState.startX = e.clientX;
this.resizeState.startY = e.clientY;
this.resizeState.startWidth = this.toolbar.clientWidth;
this.resizeState.startHeight = this.toolbar.clientHeight;
this.resizeHandle.style.cursor = 'se-resize';
},
onResizeDrag(e) {
if (!this.resizeState.isResizing) return;
const dx = e.clientX - this.resizeState.startX;
const dy = e.clientY - this.resizeState.startY;
this.size.width = Math.max(this.minWidth, this.resizeState.startWidth + dx);
this.size.height = Math.min(this.maxHeight, Math.max(this.minHeight, this.resizeState.startHeight + dy));
this.toolbar.style.width = `${this.size.width}px`;
this.toolbar.style.height = `${this.size.height}px`;
this.onResize();
},
endResize() {
if (!this.resizeState.isResizing) return;
this.resizeState.isResizing = false;
this.resizeHandle.style.cursor = 'se-resize';
this.savePositionAndSize();
}
};
app.registerExtension({
name: "KayTool.ResourceMonitor",
async setup() {
await KayResourceMonitor.init();
api.fetchApi("/kaytool/start_monitor", { method: "POST" });
const showMenuButton = new (await import("/scripts/ui/components/button.js")).ComfyButton({
content: "𝙆 Monitor",
action: () => {
if (KayResourceMonitor.isVisible) {
KayResourceMonitor.hide();
showMenuButton.tooltip = "Show Monitor";
} else {
KayResourceMonitor.show();
showMenuButton.tooltip = "Hide Monitor";
}
},
tooltip: KayResourceMonitor.isVisible ? "Hide Monitor" : "Show Monitor"
});
KayResourceMonitor.menuButton = showMenuButton.element;
// 设置项的 onChange 只在开关被拨动时触发,页面刷新走的是这里,
// 所以初始显隐必须自己按设置来一次,否则关掉的按钮一刷新就又冒出来。
showMenuButton.element.style.display = KayResourceMonitor.isEnabled ? '' : 'none';
if (app.menu?.settingsGroup) {
app.menu.settingsGroup.append(showMenuButton);
}
window.KayResourceMonitor = KayResourceMonitor;
}
});