Files
rookiestar28 6f5d8c06e9 feat(preflight): align inactive branch diagnostics
Refresh host compatibility anchors and governance expectations for current ComfyUI, frontend, and desktop references.

Add inactive branch suppression for workflow portability and preflight diagnostics, including Explorer rendering and regression coverage.

Validation: powershell -File scripts/run_full_tests_windows.ps1 passed.
2026-04-26 20:06:03 +08:00

433 lines
18 KiB
JavaScript

import { openclawApi } from "../openclaw_api.js";
import { makeEl, showToast, parseJsonOrThrow } from "../openclaw_utils.js";
/**
* F28: Explorer Tab
* Visualizes available specific Nodes and Models to help debugging.
* Includes R42 Preflight Diagnostics and R47 Workflow Checkpoints.
*/
export const ExplorerTab = {
id: "explorer",
title: "Explorer",
icon: "pi pi-compass",
render: async (container) => {
container.innerHTML = "";
// Layout: Sidebar (Inventory/Snapshots) + Main (Preflight/Details)
const layout = makeEl("div", "openclaw-explorer-layout openclaw-explorer-layout moltbot-explorer-layout");
layout.style.display = "flex";
layout.style.height = "100%";
layout.style.gap = "1rem";
const leftPanel = makeEl("div", "openclaw-explorer-sidebar openclaw-explorer-sidebar moltbot-explorer-sidebar");
leftPanel.style.flex = "1";
leftPanel.style.display = "flex";
leftPanel.style.flexDirection = "column";
const rightPanel = makeEl("div", "openclaw-explorer-main openclaw-explorer-main moltbot-explorer-main");
rightPanel.style.flex = "2";
rightPanel.style.display = "flex";
rightPanel.style.flexDirection = "column";
layout.appendChild(leftPanel);
layout.appendChild(rightPanel);
container.appendChild(layout);
// --- Left Panel Tabs ---
const leftTabs = makeEl("div", "openclaw-subtabs openclaw-subtabs moltbot-subtabs");
leftTabs.style.display = "flex";
leftTabs.style.gap = "10px";
leftTabs.style.marginBottom = "10px";
const tabInv = makeEl("button", "openclaw-btn openclaw-btn moltbot-btn active", "Inventory");
const tabSnaps = makeEl("button", "openclaw-btn openclaw-btn moltbot-btn", "Snapshots");
leftTabs.appendChild(tabInv);
leftTabs.appendChild(tabSnaps);
leftPanel.appendChild(leftTabs);
// Content Areas
const invContent = makeEl("div", "openclaw-tab-content openclaw-tab-content moltbot-tab-content active");
invContent.style.flex = "1";
invContent.style.display = "flex";
invContent.style.flexDirection = "column";
const snapsContent = makeEl("div", "openclaw-tab-content openclaw-tab-content moltbot-tab-content");
snapsContent.style.flex = "1";
snapsContent.style.display = "none";
snapsContent.style.flexDirection = "column";
leftPanel.appendChild(invContent);
leftPanel.appendChild(snapsContent);
// Switching Logic
tabInv.onclick = () => {
tabInv.classList.add("active");
tabSnaps.classList.remove("active");
invContent.style.display = "flex";
snapsContent.style.display = "none";
};
tabSnaps.onclick = () => {
tabSnaps.classList.add("active");
tabInv.classList.remove("active");
snapsContent.style.display = "flex";
invContent.style.display = "none";
loadSnapshots();
};
// --- Inventory Content ---
const searchInput = makeEl("input", "openclaw-input openclaw-input moltbot-input");
searchInput.placeholder = "Search nodes or models...";
searchInput.style.marginBottom = "10px";
const invList = makeEl("div", "openclaw-inventory-list openclaw-inventory-list moltbot-inventory-list");
invList.style.flex = "1";
invList.style.overflowY = "auto";
invList.style.border = "1px solid var(--border-color, #444)";
invList.style.padding = "0.5rem";
const invStatus = makeEl("div", "openclaw-inventory-status openclaw-inventory-status moltbot-inventory-status");
invStatus.style.fontSize = "0.85em";
invStatus.style.opacity = "0.8";
invStatus.style.marginBottom = "8px";
invContent.appendChild(searchInput);
invContent.appendChild(invStatus);
invContent.appendChild(invList);
// --- Snapshots Content ---
const snapList = makeEl("div", "openclaw-snapshot-list openclaw-snapshot-list moltbot-snapshot-list");
snapList.style.flex = "1";
snapList.style.overflowY = "auto";
snapList.style.border = "1px solid var(--border-color, #444)";
snapList.style.padding = "0.5rem";
snapsContent.appendChild(snapList);
// --- Right Panel: Preflight Diagnostics ---
const diagHeader = makeEl("h3", "", "Preflight Diagnostics");
diagHeader.style.marginTop = "0";
const diagDesc = makeEl("p", "", "Paste workflow JSON or API prompt JSON to check for missing nodes/models compatible with this environment.");
diagDesc.style.fontSize = "0.9em";
diagDesc.style.opacity = "0.8";
const jsonInput = makeEl("textarea", "openclaw-input openclaw-input moltbot-input");
jsonInput.placeholder = 'Paste workflow JSON here... {"3": {"class_type": ...}}';
jsonInput.style.flex = "1";
jsonInput.style.fontFamily = "monospace";
jsonInput.style.resize = "none";
jsonInput.style.marginBottom = "10px";
const actionsRow = makeEl("div");
actionsRow.style.display = "flex";
actionsRow.style.gap = "10px";
const runBtn = makeEl("button", "openclaw-btn openclaw-btn moltbot-btn primary", "Run Preflight");
const clearBtn = makeEl("button", "openclaw-btn openclaw-btn moltbot-btn", "Clear");
actionsRow.appendChild(runBtn);
actionsRow.appendChild(clearBtn);
const resultsArea = makeEl("div", "openclaw-preflight-results openclaw-preflight-results moltbot-preflight-results");
resultsArea.style.marginTop = "10px";
resultsArea.style.padding = "10px";
resultsArea.style.border = "1px solid var(--border-color, #444)";
resultsArea.style.minHeight = "100px";
resultsArea.style.display = "none";
rightPanel.appendChild(diagHeader);
rightPanel.appendChild(diagDesc);
rightPanel.appendChild(jsonInput);
rightPanel.appendChild(actionsRow);
rightPanel.appendChild(resultsArea);
// --- Logic ---
let inventoryData = null;
let inventoryRefreshTimer = null;
// Helper: Debounce
function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
// Fetch Inventory
async function loadInventory() {
if (inventoryRefreshTimer) {
clearTimeout(inventoryRefreshTimer);
inventoryRefreshTimer = null;
}
invList.innerHTML = "Loading...";
invStatus.textContent = "";
const res = await openclawApi.getInventory();
if (res.ok) {
inventoryData = res.data;
renderInventoryStatus(res.data);
renderInventoryList(res.data, searchInput.value);
if (res.data?.scan_state === "refreshing" || (res.data?.stale && !res.data?.last_error)) {
inventoryRefreshTimer = window.setTimeout(() => {
loadInventory().catch(() => { });
}, 1500);
}
} else {
invStatus.textContent = "";
invList.innerHTML = `<div class="error">Failed to load inventory: ${res.error}</div>`;
}
}
function renderInventoryStatus(data) {
if (!data) {
invStatus.textContent = "";
return;
}
const bits = [];
if (data.scan_state === "refreshing") {
bits.push("Refreshing inventory snapshot...");
} else if (data.scan_state === "error") {
bits.push("Inventory refresh failed.");
}
if (typeof data.snapshot_ts === "number" && Number.isFinite(data.snapshot_ts)) {
const stamp = new Date(data.snapshot_ts * 1000);
bits.push(`Snapshot: ${stamp.toLocaleString()}`);
} else {
bits.push("Snapshot: pending");
}
if (data.stale) {
bits.push("State: stale");
}
if (data.last_error) {
bits.push(`Last error: ${data.last_error}`);
}
invStatus.textContent = bits.join(" ");
}
async function loadSnapshots() {
snapList.innerHTML = "Loading...";
const res = await openclawApi.listCheckpoints();
if (res.ok) {
renderSnapshots(res.data.checkpoints || []);
} else {
snapList.innerHTML = `<div class="error">Failed to load snapshots: ${res.error}</div>`;
}
}
function renderInventoryList(data, query = "") {
invList.innerHTML = "";
const q = query.toLowerCase();
const MAX_ITEMS_PER_CAT = 100;
// Nodes
const nodes = data.nodes || [];
const filteredNodes = nodes.filter(n => n.toLowerCase().includes(q));
if (filteredNodes.length > 0) {
const h = makeEl("h4", "", `Nodes (${filteredNodes.length})`);
h.style.margin = "5px 0";
invList.appendChild(h);
filteredNodes.slice(0, MAX_ITEMS_PER_CAT).forEach(n => {
const row = makeEl("div", "openclaw-inv-item openclaw-inv-item moltbot-inv-item", n);
row.style.fontSize = "0.9em";
row.style.padding = "2px 0";
invList.appendChild(row);
});
if (filteredNodes.length > MAX_ITEMS_PER_CAT) {
const more = makeEl("div", "", `...and ${filteredNodes.length - MAX_ITEMS_PER_CAT} more`);
more.style.fontStyle = "italic";
more.style.opacity = "0.6";
more.style.fontSize = "0.8em";
invList.appendChild(more);
}
}
// Models
const models = data.models || {};
for (const [type, list] of Object.entries(models)) {
const filteredVars = list.filter(m => m.toLowerCase().includes(q));
if (filteredVars.length > 0) {
const h = makeEl("h4", "", `${type} (${filteredVars.length})`);
h.style.margin = "10px 0 5px 0";
invList.appendChild(h);
filteredVars.slice(0, MAX_ITEMS_PER_CAT).forEach(m => {
const row = makeEl("div", "openclaw-inv-item openclaw-inv-item moltbot-inv-item", m);
row.style.fontSize = "0.9em";
row.style.padding = "2px 0";
row.title = m; // tooltip
invList.appendChild(row);
});
if (filteredVars.length > MAX_ITEMS_PER_CAT) {
const more = makeEl("div", "", `...and ${filteredVars.length - MAX_ITEMS_PER_CAT} more`);
more.style.fontStyle = "italic";
more.style.opacity = "0.6";
more.style.fontSize = "0.8em";
invList.appendChild(more);
}
}
}
}
// Debounced Input
searchInput.addEventListener("input", debounce(() => {
if (inventoryData) renderInventoryList(inventoryData, searchInput.value);
}, 300));
// Preflight Action
runBtn.onclick = async () => {
const jsonStr = jsonInput.value.trim();
if (!jsonStr) return;
resultsArea.style.display = "block";
resultsArea.innerHTML = "Running diagnostics...";
let workflow;
try {
workflow = parseJsonOrThrow(jsonStr, "Invalid JSON");
if (workflow.prompt) workflow = workflow.prompt;
else if (workflow.workflow) workflow = workflow.workflow;
} catch (e) {
resultsArea.innerHTML = `<div class="error">Invalid JSON: ${e.message}</div>`;
return;
}
const res = await openclawApi.runPreflight(workflow);
if (res.ok) {
renderResults(res.data, workflow);
} else {
resultsArea.innerHTML = `<div class="error">Error: ${res.error}</div>`;
}
};
clearBtn.onclick = () => {
jsonInput.value = "";
resultsArea.style.display = "none";
resultsArea.innerHTML = "";
};
function renderResults(report, workflow) {
resultsArea.innerHTML = "";
const headerRow = makeEl("div");
headerRow.style.display = "flex";
headerRow.style.justifyContent = "space-between";
headerRow.style.alignItems = "center";
const statusColor = report.ok ? "var(--success-color, #4caf50)" : "var(--error-color, #f44336)";
const summaryEl = makeEl("div", "", "");
summaryEl.style.color = statusColor;
summaryEl.style.fontWeight = "bold";
summaryEl.textContent = report.ok ? "✅ Workflow Compatible" : "❌ Issues Detected";
const saveBtn = makeEl("button", "openclaw-btn openclaw-btn moltbot-btn", "Save Snapshot");
saveBtn.onclick = async () => {
const name = prompt("Snapshot Name:", "New Snapshot");
if (name) {
const res = await openclawApi.createCheckpoint(name, workflow);
if (res.ok) {
showToast("Snapshot saved");
if (tabSnaps.classList.contains("active")) loadSnapshots();
} else {
showToast("Save failed: " + res.error, "error");
}
}
};
headerRow.appendChild(summaryEl);
headerRow.appendChild(saveBtn);
resultsArea.appendChild(headerRow);
// R90: Known Replacements
const MISSING_NODE_REPLACEMENTS = {
"CLIPTextEncode": "CLIPTextEncodeSDXL",
"VAEDecode": "VAEDecodeTiled",
"KSampler": "KSamplerAdvanced",
// Add more common migration mappings here
};
if (report.summary.missing_nodes > 0) {
const section = makeEl("div");
section.innerHTML = `<h4>Missing Nodes (${report.summary.missing_nodes})</h4>`;
const ul = makeEl("ul");
report.missing_nodes.forEach(m => {
const li = makeEl("li", "", "");
// Safe text insertion
const textSpan = document.createElement("span");
textSpan.textContent = `${m.class_type} (x${m.count})`;
li.appendChild(textSpan);
if (MISSING_NODE_REPLACEMENTS[m.class_type]) {
const hint = document.createElement("span");
hint.style.opacity = "0.7";
hint.style.fontSize = "0.9em";
hint.innerHTML = ` (Try: <code>${MISSING_NODE_REPLACEMENTS[m.class_type]}</code>)`;
li.appendChild(hint);
}
ul.appendChild(li);
});
section.appendChild(ul);
resultsArea.appendChild(section);
}
if (report.summary.missing_models > 0) {
const section = makeEl("div");
section.innerHTML = `<h4>Missing Models (${report.summary.missing_models})</h4>`;
const ul = makeEl("ul");
report.missing_models.forEach(m => {
const li = makeEl("li", "", `${m.type}: ${m.name} (x${m.count})`);
ul.appendChild(li);
});
section.appendChild(ul);
resultsArea.appendChild(section);
}
const suppressedNodeCount = Number(report.summary?.suppressed_missing_nodes || 0);
const suppressedModelCount = Number(report.summary?.suppressed_missing_models || 0);
const suppressedTotal = suppressedNodeCount + suppressedModelCount;
if (suppressedTotal > 0) {
const section = makeEl("div", "openclaw-preflight-suppressed");
const heading = makeEl("h4", "", `Inactive Branch Findings Suppressed (${suppressedTotal})`);
section.appendChild(heading);
const note = makeEl(
"div",
"",
"Muted or bypassed workflow branches are not counted as actionable missing dependencies."
);
note.style.opacity = "0.75";
note.style.fontSize = "0.9em";
section.appendChild(note);
const ul = makeEl("ul");
(report.suppressed_missing_nodes || []).forEach(m => {
const li = makeEl(
"li",
"",
`${m.node_id || "unknown"}: ${m.class_type || "unknown node"}`
);
ul.appendChild(li);
});
(report.suppressed_missing_models || []).forEach(m => {
const li = makeEl(
"li",
"",
`${m.node_id || "unknown"}: ${m.type || "model"} ${m.name || "unknown model"}`
);
ul.appendChild(li);
});
section.appendChild(ul);
resultsArea.appendChild(section);
}
}
// Initial Load
loadInventory();
}
};