feat: implement deep auto-refresh for ComfyUI bridge with backend reset and frontend combo update
This commit is contained in:
@@ -55,7 +55,6 @@ public class Main extends JFrame {
|
||||
private final IModelValidator modelValidator;
|
||||
private final de.tki.comfymodels.service.impl.RestBridgeService restBridge;
|
||||
private final de.tki.comfymodels.service.impl.ArchiveService archiveService;
|
||||
private final de.tki.comfymodels.service.impl.ComfyUIBridgeClient bridgeClient;
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
@@ -90,8 +89,7 @@ public class Main extends JFrame {
|
||||
public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager,
|
||||
IWorkflowService workflowService, IModelSearchService searchService,
|
||||
IModelValidator modelValidator, de.tki.comfymodels.service.impl.RestBridgeService restBridge,
|
||||
de.tki.comfymodels.service.impl.ArchiveService archiveService,
|
||||
de.tki.comfymodels.service.impl.ComfyUIBridgeClient bridgeClient) {
|
||||
de.tki.comfymodels.service.impl.ArchiveService archiveService) {
|
||||
this.analyzer = analyzer;
|
||||
this.downloadManager = downloadManager;
|
||||
this.workflowService = workflowService;
|
||||
@@ -99,7 +97,6 @@ public class Main extends JFrame {
|
||||
this.modelValidator = modelValidator;
|
||||
this.restBridge = restBridge;
|
||||
this.archiveService = archiveService;
|
||||
this.bridgeClient = bridgeClient;
|
||||
}
|
||||
|
||||
public void launch(String[] args) {
|
||||
@@ -354,7 +351,7 @@ public class Main extends JFrame {
|
||||
try {
|
||||
configService.unlock(pass);
|
||||
restBridge.setApiToken(configService.getApiToken());
|
||||
syncBridgeToken();
|
||||
syncBridgeFiles(); // NEW: Automatically sync code & token on every unlock
|
||||
|
||||
// NEW: Auto-import model list on first start or vault reset
|
||||
if (configService.isVaultFresh()) {
|
||||
@@ -769,7 +766,6 @@ public class Main extends JFrame {
|
||||
() -> SwingUtilities.invokeLater(() -> {
|
||||
downloadButton.setEnabled(true);
|
||||
statusLabel.setText("Queue finished.");
|
||||
bridgeClient.notifyModelDownloaded();
|
||||
if (configService.isShutdownAfterDownloadEnabled()) performSystemShutdown();
|
||||
})
|
||||
);
|
||||
@@ -843,7 +839,7 @@ public class Main extends JFrame {
|
||||
return;
|
||||
}
|
||||
configService.setComfyUIPath(selectedPath);
|
||||
syncBridgeToken();
|
||||
syncBridgeFiles();
|
||||
JOptionPane.showMessageDialog(dialog, "API Token synchronized successfully.");
|
||||
});
|
||||
|
||||
@@ -1447,6 +1443,36 @@ public class Main extends JFrame {
|
||||
JTable archiveTable = new JTable(archiveTableModel);
|
||||
archiveTable.setRowHeight(25);
|
||||
archiveTable.getColumnModel().getColumn(0).setMaxWidth(50);
|
||||
|
||||
// Enable sorting with custom comparator for human-readable sizes
|
||||
javax.swing.table.TableRowSorter<DefaultTableModel> archiveSorter = new javax.swing.table.TableRowSorter<>(archiveTableModel);
|
||||
archiveSorter.setComparator(3, (s1, s2) -> Long.compare(parseSizeToBytes((String)s1), parseSizeToBytes((String)s2)));
|
||||
archiveSorter.setSortable(0, false); // Disable sorting for checkbox column to allow "Select All"
|
||||
archiveTable.setRowSorter(archiveSorter);
|
||||
|
||||
// Add "Select All" functionality to header
|
||||
archiveTable.getTableHeader().addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
int viewIdx = archiveTable.columnAtPoint(e.getPoint());
|
||||
if (viewIdx == -1) return;
|
||||
int modelIdx = archiveTable.convertColumnIndexToModel(viewIdx);
|
||||
if (modelIdx == 0) {
|
||||
boolean allSelected = true;
|
||||
for (int i = 0; i < archiveTableModel.getRowCount(); i++) {
|
||||
if (!(Boolean) archiveTableModel.getValueAt(i, 0)) {
|
||||
allSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean newValue = !allSelected;
|
||||
for (int i = 0; i < archiveTableModel.getRowCount(); i++) {
|
||||
archiveTableModel.setValueAt(newValue, i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane archiveScroll = new JScrollPane(archiveTable);
|
||||
archivePanel.add(archiveScroll, BorderLayout.CENTER);
|
||||
|
||||
@@ -1494,6 +1520,36 @@ public class Main extends JFrame {
|
||||
JTable restoreTable = new JTable(restoreTableModel);
|
||||
restoreTable.setRowHeight(25);
|
||||
restoreTable.getColumnModel().getColumn(0).setMaxWidth(50);
|
||||
|
||||
// Enable sorting with custom comparator for human-readable sizes
|
||||
javax.swing.table.TableRowSorter<DefaultTableModel> restoreSorter = new javax.swing.table.TableRowSorter<>(restoreTableModel);
|
||||
restoreSorter.setComparator(3, (s1, s2) -> Long.compare(parseSizeToBytes((String)s1), parseSizeToBytes((String)s2)));
|
||||
restoreSorter.setSortable(0, false); // Disable sorting for checkbox column to allow "Select All"
|
||||
restoreTable.setRowSorter(restoreSorter);
|
||||
|
||||
// Add "Select All" functionality to header
|
||||
restoreTable.getTableHeader().addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
int viewIdx = restoreTable.columnAtPoint(e.getPoint());
|
||||
if (viewIdx == -1) return;
|
||||
int modelIdx = restoreTable.convertColumnIndexToModel(viewIdx);
|
||||
if (modelIdx == 0) {
|
||||
boolean allSelected = true;
|
||||
for (int i = 0; i < restoreTableModel.getRowCount(); i++) {
|
||||
if (!(Boolean) restoreTableModel.getValueAt(i, 0)) {
|
||||
allSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean newValue = !allSelected;
|
||||
for (int i = 0; i < restoreTableModel.getRowCount(); i++) {
|
||||
restoreTableModel.setValueAt(newValue, i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane restoreScroll = new JScrollPane(restoreTable);
|
||||
restorePanel.add(restoreScroll, BorderLayout.CENTER);
|
||||
|
||||
@@ -1611,6 +1667,7 @@ public class Main extends JFrame {
|
||||
JOptionPane.showMessageDialog(this, finalSuccess + " models successfully " + title.toLowerCase() + "ed.",
|
||||
"Operation Complete", JOptionPane.INFORMATION_MESSAGE);
|
||||
analyzeJsonContent();
|
||||
downloadManager.notifyComfyUI();
|
||||
});
|
||||
}).start();
|
||||
|
||||
@@ -1735,7 +1792,7 @@ public class Main extends JFrame {
|
||||
helpDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void syncBridgeToken() {
|
||||
private void syncBridgeFiles() {
|
||||
String comfyPath = configService.getComfyUIPath();
|
||||
if (comfyPath == null || comfyPath.isEmpty()) return;
|
||||
|
||||
@@ -1747,8 +1804,19 @@ public class Main extends JFrame {
|
||||
|
||||
Path targetDir = customNodesDir.resolve("comfyui-model-downloader");
|
||||
if (Files.exists(targetDir) && Files.isDirectory(targetDir)) {
|
||||
writeExtensionConfig(targetDir.toFile());
|
||||
System.out.println("Synchronized API token with ComfyUI bridge at: " + targetDir);
|
||||
try {
|
||||
// Always sync latest code files from resources to target
|
||||
extractResource("/comfyui-bridge/__init__.py", targetDir.resolve("__init__.py").toFile());
|
||||
Path webDir = targetDir.resolve("web");
|
||||
Files.createDirectories(webDir);
|
||||
extractResource("/comfyui-bridge/web/downloader.js", webDir.resolve("downloader.js").toFile());
|
||||
|
||||
// Also update config
|
||||
writeExtensionConfig(targetDir.toFile());
|
||||
System.out.println("[Bridge-Sync] Successfully synchronized latest bridge code and token.");
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Bridge-Sync] Failed to sync code: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ public interface IDownloadManager {
|
||||
void stop();
|
||||
boolean isPaused();
|
||||
|
||||
/**
|
||||
* Triggers a refresh signal to the ComfyUI bridge.
|
||||
*/
|
||||
void notifyComfyUI();
|
||||
|
||||
/**
|
||||
* Returns a map of current statuses for each model in the queue.
|
||||
*/
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package de.tki.comfymodels.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
@Service
|
||||
public class ComfyUIBridgeClient {
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(2))
|
||||
.build();
|
||||
|
||||
private String comfyUrl = "http://127.0.0.1:8188";
|
||||
|
||||
public void setComfyUrl(String url) {
|
||||
if (url != null && !url.isEmpty()) {
|
||||
this.comfyUrl = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyModelDownloaded() {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(comfyUrl + "/kippster/model-downloaded"))
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
|
||||
System.out.println("Notifying ComfyUI at: " + request.uri());
|
||||
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenAccept(response -> {
|
||||
if (response.statusCode() >= 200 && response.statusCode() < 300) {
|
||||
System.out.println("ComfyUI notified successfully: " + response.body());
|
||||
} else {
|
||||
System.err.println("Failed to notify ComfyUI. Status: " + response.statusCode());
|
||||
}
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
System.err.println("Could not reach ComfyUI for refresh: " + ex.getMessage());
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error creating notification request: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,15 @@ public class ConfigService {
|
||||
save();
|
||||
}
|
||||
|
||||
public String getComfyUIUrl() { return settings.optString("comfyui_url", "http://127.0.0.1:8188"); }
|
||||
public void setComfyUIUrl(String url) {
|
||||
if (url != null && !url.isEmpty()) {
|
||||
String sanitized = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
|
||||
settings.put("comfyui_url", sanitized);
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
public String getApiToken() {
|
||||
String token = settings.optString("api_token", "");
|
||||
if (token.isEmpty() && isUnlocked()) {
|
||||
|
||||
@@ -121,20 +121,68 @@ public class DefaultDownloadManager implements IDownloadManager {
|
||||
}
|
||||
|
||||
// NEU: Benachrichtigt ComfyUI
|
||||
private void notifyComfyUI() {
|
||||
@Override
|
||||
public void notifyComfyUI() {
|
||||
String comfyUrl = configService.getComfyUIUrl();
|
||||
if (sendRefreshPing(comfyUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-Discovery: If configured URL fails, search for ComfyUI on common ports
|
||||
System.out.println("🔍 [Model-Downloader] ComfyUI unter " + comfyUrl + " nicht erreicht. Suche automatisch...");
|
||||
String discoveredUrl = discoverComfyUrl();
|
||||
if (discoveredUrl != null) {
|
||||
System.out.println("✨ [Model-Downloader] ComfyUI automatisch gefunden unter: " + discoveredUrl);
|
||||
configService.setComfyUIUrl(discoveredUrl);
|
||||
sendRefreshPing(discoveredUrl);
|
||||
} else {
|
||||
System.err.println("❌ [Model-Downloader] ComfyUI konnte nicht automatisch gefunden werden. Bitte stelle sicher, dass es läuft.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendRefreshPing(String url) {
|
||||
try {
|
||||
System.out.println("🚀 [Model-Downloader] Sende Refresh-Ping an ComfyUI (Port 8188)...");
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:8188/kippster/model-downloaded"))
|
||||
.uri(URI.create(url + "/kippster/model-downloaded"))
|
||||
.timeout(java.time.Duration.ofSeconds(2))
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
|
||||
// Synchron senden, damit wir sehen, was passiert
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
System.out.println("✅ [Model-Downloader] ComfyUI hat geantwortet mit Status: " + response.statusCode());
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ [Model-Downloader] Konnte ComfyUI nicht erreichen. Läuft es auf Port 8188? Fehler: " + e.getMessage());
|
||||
if (response.statusCode() == 200) {
|
||||
System.out.println("✅ [Model-Downloader] ComfyUI (" + url + ") erfolgreich benachrichtigt.");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String discoverComfyUrl() {
|
||||
// Scan typical ComfyUI ports, including common alternatives like 8000
|
||||
int[] commonPorts = {8188, 8000, 8189, 8190, 3000, 8080};
|
||||
for (int port : commonPorts) {
|
||||
if (checkPort(port)) return "http://127.0.0.1:" + port;
|
||||
}
|
||||
|
||||
// Expanded range scan
|
||||
for (int port = 8191; port <= 8199; port++) {
|
||||
if (checkPort(port)) return "http://127.0.0.1:" + port;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean checkPort(int port) {
|
||||
String url = "http://127.0.0.1:" + port;
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url + "/system_stats"))
|
||||
.timeout(java.time.Duration.ofMillis(300))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return response.statusCode() == 200 && response.body().contains("system");
|
||||
} catch (Exception ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isSelected(int index) {
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
# ComfyUI Model Downloader Bridge
|
||||
import server
|
||||
import folder_paths
|
||||
import os
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
|
||||
print("\033[95m[Model-Downloader] UI Extension Bridge active\033[0m")
|
||||
|
||||
@server.PromptServer.instance.routes.post("/kippster/model-downloaded")
|
||||
async def model_downloaded(request):
|
||||
# Dieser Text MUSS in der ComfyUI-Konsole auftauchen, wenn Java fertig ist!
|
||||
print("\033[92m🔥 [Model-Downloader] PING ERHALTEN! Sende Signal ans Frontend...\033[0m")
|
||||
print("\033[92m🔥 [Model-Downloader] PING ERHALTEN! Erzwinge harten Backend-Reset...\033[0m")
|
||||
|
||||
try:
|
||||
# 1. Dateisystem-Cache löschen (Windows braucht hier Zeit)
|
||||
await asyncio.sleep(0.5)
|
||||
if hasattr(folder_paths, "base_path_layers"):
|
||||
for layer in folder_paths.base_path_layers.values():
|
||||
layer.folder_names.clear()
|
||||
if hasattr(folder_paths, "folder_names"):
|
||||
folder_paths.folder_names.clear()
|
||||
|
||||
# 2. Den Definitions-Cache von ComfyUI löschen
|
||||
# Das zwingt ComfyUI, die Dropdown-Listen für den Browser komplett neu zu generieren
|
||||
server.PromptServer.instance.object_info = None
|
||||
|
||||
# 3. Aktiven Scan triggern
|
||||
for folder in ["checkpoints", "loras", "vae", "controlnet", "diffusion_models", "upscale_models", "clip"]:
|
||||
try:
|
||||
folder_paths.get_filename_list(folder)
|
||||
except: pass
|
||||
|
||||
print("[Model-Downloader] Backend-Reset und Scan abgeschlossen.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Model-Downloader] Fehler beim Reset: {e}")
|
||||
|
||||
# 4. Signale an alle Browser (jetzt mit frischen Daten)
|
||||
server.PromptServer.instance.send_sync("refresh", {})
|
||||
server.PromptServer.instance.send_sync("kippster-refresh-ui", {"message": "ready"})
|
||||
return web.json_response({"status": "success", "message": "UI refreshed"})
|
||||
|
||||
return web.json_response({"status": "success", "message": "Backend reset and rescan complete"})
|
||||
|
||||
WEB_DIRECTORY = "web"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
|
||||
@@ -1,370 +1,104 @@
|
||||
// Try different ways to get the ComfyUI app and api objects
|
||||
// ComfyUI Model Downloader - Bridge Extension
|
||||
let app = null;
|
||||
let api = null;
|
||||
|
||||
const tryImports = async () => {
|
||||
const paths = [
|
||||
"../../scripts/app.js",
|
||||
"/scripts/app.js",
|
||||
"../../../scripts/app.js"
|
||||
];
|
||||
const apiPaths = [
|
||||
"../../scripts/api.js",
|
||||
"/scripts/api.js",
|
||||
"../../../scripts/api.js"
|
||||
];
|
||||
const initializeExtension = async () => {
|
||||
const paths = ["../../scripts/app.js", "/scripts/app.js", "../../../scripts/app.js"];
|
||||
const apiPaths = ["../../scripts/api.js", "/scripts/api.js", "../../../scripts/api.js"];
|
||||
for (const path of paths) { try { const mod = await import(path); if (mod.app) { app = mod.app; break; } } catch (e) {} }
|
||||
for (const path of apiPaths) { try { const mod = await import(path); if (mod.api) { api = mod.api; break; } } catch (e) {} }
|
||||
|
||||
for (const path of paths) {
|
||||
if (!app || !api) { setTimeout(initializeExtension, 2000); return; }
|
||||
|
||||
api.addEventListener("kippster-refresh-ui", async (event) => {
|
||||
console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Starte Deep-Node-Update...", "background: #ff0000; color: #fff; font-size: 14px;");
|
||||
|
||||
try {
|
||||
const mod = await import(path);
|
||||
if (mod.app) {
|
||||
app = mod.app;
|
||||
console.log(`[Model-Downloader] Successfully imported app from ${path}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
// 1. Backend Zeit zum Scannen geben
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
for (const path of apiPaths) {
|
||||
try {
|
||||
const mod = await import(path);
|
||||
if (mod.api) {
|
||||
api = mod.api;
|
||||
console.log(`[Model-Downloader] Successfully imported api from ${path}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
await tryImports();
|
||||
|
||||
// NEU: Dynamischer Import für den WebSocket-Listener
|
||||
const setupListener = async () => {
|
||||
const apiPaths = [
|
||||
"../../scripts/api.js",
|
||||
"/scripts/api.js",
|
||||
"../../../scripts/api.js"
|
||||
];
|
||||
|
||||
for (const path of apiPaths) {
|
||||
try {
|
||||
const mod = await import(path);
|
||||
if (mod.api) {
|
||||
console.log("%c[Model-Downloader] WebSocket Listener erfolgreich verknüpft!", "color: #00ff00; font-weight: bold;");
|
||||
|
||||
mod.api.addEventListener("kippster-refresh-ui", (event) => {
|
||||
console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Lade UI neu...", "background: #ff0000; color: #fff; font-size: 14px;");
|
||||
|
||||
if (app && typeof app.refresh === 'function') {
|
||||
app.refresh(); // Aktualisiert die Model-Dropdowns!
|
||||
|
||||
// Kleines UI-Popup zur Bestätigung
|
||||
if (app.ui?.dialog?.show) {
|
||||
app.ui.dialog.show("✅ Downloads abgeschlossen! Modelle sind jetzt im Dropdown.");
|
||||
} else {
|
||||
alert("✅ Downloads abgeschlossen! Modelle aktualisiert.");
|
||||
// 2. Definitionen neu laden (Zwingt den Browser die neuesten Modell-Listen zu kennen)
|
||||
const nodeDefs = await api.getNodeDefs();
|
||||
|
||||
// 3. Bestehende Nodes auf dem Canvas aktualisieren
|
||||
if (app.graph && app.graph._nodes) {
|
||||
console.log("[Model-Downloader] Aktualisiere Dropdowns in " + app.graph._nodes.length + " Nodes...");
|
||||
for (const node of app.graph._nodes) {
|
||||
const def = nodeDefs[node.type];
|
||||
if (def && def.input && def.input.required) {
|
||||
for (const widgetName in def.input.required) {
|
||||
const inputDef = def.input.required[widgetName];
|
||||
// Falls es ein Dropdown (Combo) ist
|
||||
if (Array.isArray(inputDef[0])) {
|
||||
const newValues = inputDef[0];
|
||||
const widget = node.widgets?.find(w => w.name === widgetName);
|
||||
if (widget && widget.type === "combo") {
|
||||
widget.options.values = newValues;
|
||||
|
||||
// Falls das gewählte Modell jetzt da ist, "heilen" wir den Node
|
||||
if (newValues.includes(widget.value)) {
|
||||
node.color = "";
|
||||
node.bgcolor = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
break; // Erfolgreich gefunden, Schleife abbrechen
|
||||
if (node.onRefresh) node.onRefresh();
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
await setupListener();
|
||||
if (app.refresh) await app.refresh();
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
|
||||
if (app.ui?.dialog?.show) {
|
||||
app.ui.dialog.show("✅ Sync abgeschlossen! Neue Modelle wurden in die Dropdowns geladen.");
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("[Model-Downloader] Fehler beim Deep-Node-Update:", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Define setup logic that can be called by extension or as fallback
|
||||
const initializeExtension = async () => {
|
||||
let apiToken = null;
|
||||
|
||||
// We removed the old inline listener here to avoid duplicates
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const baseUrl = new URL(".", import.meta.url).href;
|
||||
const configUrl = `${baseUrl}config.json?v=${Date.now()}`;
|
||||
console.log(`[Model-Downloader] Loading config from: ${configUrl}`);
|
||||
const response = await fetch(configUrl);
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
apiToken = config.token;
|
||||
if (apiToken) {
|
||||
const masked = apiToken.substring(0, 4) + "..." + apiToken.substring(apiToken.length - 4);
|
||||
console.log(`[Model-Downloader] Token loaded successfully: ${masked}`);
|
||||
} else {
|
||||
console.warn("[Model-Downloader] Token in config.json is empty!");
|
||||
}
|
||||
} else {
|
||||
console.error(`[Model-Downloader] Could not load config.json (Status: ${response.status})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Model-Downloader] Failed to load config.json:", e);
|
||||
if (response.ok) { const config = await response.json(); apiToken = config.token; }
|
||||
} catch (e) {}
|
||||
};
|
||||
await loadConfig();
|
||||
|
||||
const addUI = () => {
|
||||
if (!document.getElementById("tki-downloader-fab")) {
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "tki-downloader-fab";
|
||||
fab.innerHTML = "🚀";
|
||||
fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(255,204,0,0.5); border: 2px solid white;";
|
||||
fab.onclick = async () => {
|
||||
if (!app?.graph) return;
|
||||
try {
|
||||
await fetch("http://127.0.0.1:12345/import", {
|
||||
method: "POST", mode: "cors",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` },
|
||||
body: JSON.stringify(app.graph.serialize())
|
||||
});
|
||||
} catch (e) {}
|
||||
};
|
||||
document.body.appendChild(fab);
|
||||
}
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
|
||||
const notify = (msg) => {
|
||||
if (app?.ui?.dialog?.show) app.ui.dialog.show(msg);
|
||||
else if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg);
|
||||
else alert(msg);
|
||||
};
|
||||
|
||||
const showConnectionError = () => {
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.style = "background:#222; color:white; border:2px solid #ffcc00; border-radius:10px; padding:20px; width:450px; font-family:sans-serif; box-shadow:0 0 30px rgba(0,0,0,0.5); z-index:20002;";
|
||||
dialog.innerHTML = `
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:50px; margin-bottom:15px;">🚀❌</div>
|
||||
<h2 style="margin-top:0; color:#ffcc00;">Downloader nicht erreichbar</h2>
|
||||
<p style="line-height:1.5;">Die Model Downloader App antwortet nicht. Bitte stelle sicher, dass:</p>
|
||||
<ul style="text-align:left; display:inline-block; margin-bottom:20px;">
|
||||
<li>Die Java-App gestartet wurde.</li>
|
||||
<li>Port 12345 nicht blockiert wird.</li>
|
||||
</ul>
|
||||
<p>Noch nicht installiert? Lade die App hier herunter:</p>
|
||||
<a href="https://sourceforge.net/projects/comfymodeldownloader/" target="_blank" style="display:block; background:#ffcc00; color:black; text-decoration:none; padding:10px; border-radius:5px; font-weight:bold; margin-bottom:20px;">📥 Download via SourceForge</a>
|
||||
<button id="tki-error-close" style="background:#444; color:white; border:none; padding:10px 20px; border-radius:5px; cursor:pointer;">Schließen</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
dialog.querySelector("#tki-error-close").onclick = () => {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
};
|
||||
|
||||
const sendWorkflow = async () => {
|
||||
if (!app || !app.graph) return notify("❌ Fehler: ComfyUI Graph nicht bereit.");
|
||||
if (!apiToken) return notify("❌ Sicherheits-Fehler: Kein API-Token gefunden.");
|
||||
|
||||
const workflow = app.graph.serialize();
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:12345/import", {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiToken}`
|
||||
},
|
||||
body: JSON.stringify(workflow)
|
||||
});
|
||||
if (response.ok) notify("🚀 Workflow erfolgreich gesendet!");
|
||||
else notify("❌ Fehler: Downloader abgelehnt (Status: " + response.status + ")");
|
||||
} catch (e) {
|
||||
showConnectionError();
|
||||
}
|
||||
};
|
||||
|
||||
const openCleanupDialog = async () => {
|
||||
if (!apiToken) return notify("❌ Sicherheits-Fehler: Kein API-Token gefunden.");
|
||||
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.id = "tki-cleanup-dialog";
|
||||
dialog.style = "background:#222; color:white; border:2px solid #ffcc00; border-radius:10px; padding:20px; width:800px; max-height:80vh; overflow:hidden; font-family:sans-serif; box-shadow:0 0 30px rgba(0,0,0,0.5); z-index:20001;";
|
||||
|
||||
dialog.innerHTML = `
|
||||
<div id="tki-cleanup-content">
|
||||
<h2 style="margin-top:0; color:#ffcc00;">🧹 Lokale Modelle aufräumen</h2>
|
||||
<div id="tki-cleanup-loading" style="text-align:center; padding:40px;">
|
||||
<div style="border:4px solid #f3f3f3; border-top:4px solid #ffcc00; border-radius:50%; width:30px; height:30px; animation:spin 1s linear infinite; margin:0 auto 10px;"></div>
|
||||
<span>Lade lokale Modelle...</span>
|
||||
</div>
|
||||
<div id="tki-cleanup-table-container" style="display:none;">
|
||||
<div style="max-height:50vh; overflow-y:auto; margin-bottom:20px; border:1px solid #444;">
|
||||
<table style="width:100%; border-collapse:collapse; text-align:left;">
|
||||
<thead style="position:sticky; top:0; background:#333;">
|
||||
<tr>
|
||||
<th style="padding:10px;"><input type="checkbox" id="tki-select-all"></th>
|
||||
<th style="padding:10px;">Name</th>
|
||||
<th style="padding:10px;">Typ</th>
|
||||
<th style="padding:10px;">Größe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tki-cleanup-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span id="tki-cleanup-status" style="font-style:italic; color:#aaa;"></span>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<button id="tki-cleanup-cancel" style="background:#444; color:white; border:none; padding:10px 20px; border-radius:5px; cursor:pointer;">Abbrechen</button>
|
||||
<button id="tki-cleanup-archive" style="background:#ffcc00; color:white; border:none; padding:10px 20px; border-radius:5px; cursor:pointer; font-weight:bold;">Ausgewählte archivieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
`;
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
|
||||
const refreshData = async () => {
|
||||
const loading = dialog.querySelector("#tki-cleanup-loading");
|
||||
const container = dialog.querySelector("#tki-cleanup-table-container");
|
||||
loading.style.display = "block";
|
||||
container.style.display = "none";
|
||||
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:12345/api/models/local", {
|
||||
headers: { "Authorization": `Bearer ${apiToken}` }
|
||||
});
|
||||
const models = await response.json();
|
||||
|
||||
const tbody = dialog.querySelector("#tki-cleanup-tbody");
|
||||
tbody.innerHTML = models.map(m => `
|
||||
<tr style="border-bottom:1px solid #333;">
|
||||
<td style="padding:10px;"><input type="checkbox" class="tki-model-select" data-path="${m.save_path}/${m.name}"></td>
|
||||
<td style="padding:10px;">${m.name}</td>
|
||||
<td style="padding:10px;">${m.type}</td>
|
||||
<td style="padding:10px;">${m.size}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
loading.style.display = "none";
|
||||
container.style.display = "block";
|
||||
|
||||
// Wire up select all
|
||||
const selectAll = dialog.querySelector("#tki-select-all");
|
||||
const checkboxes = dialog.querySelectorAll(".tki-model-select");
|
||||
selectAll.checked = false;
|
||||
selectAll.onchange = () => checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||||
|
||||
} catch (e) {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
showConnectionError();
|
||||
}
|
||||
};
|
||||
|
||||
refreshData();
|
||||
|
||||
dialog.querySelector("#tki-cleanup-cancel").onclick = () => {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
|
||||
dialog.querySelector("#tki-cleanup-archive").onclick = async () => {
|
||||
const checkboxes = dialog.querySelectorAll(".tki-model-select");
|
||||
const selectedPaths = Array.from(checkboxes)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.getAttribute("data-path"));
|
||||
|
||||
if (selectedPaths.length === 0) return alert("Bitte wähle mindestens ein Modell aus.");
|
||||
|
||||
const status = dialog.querySelector("#tki-cleanup-status");
|
||||
const btn = dialog.querySelector("#tki-cleanup-archive");
|
||||
btn.disabled = true;
|
||||
status.textContent = `Archiviere ${selectedPaths.length} Dateien...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("http://127.0.0.1:12345/api/models/archive", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` },
|
||||
body: JSON.stringify(selectedPaths)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
alert(`Erfolgreich ${data.archived} Modelle archiviert!`);
|
||||
status.textContent = "";
|
||||
btn.disabled = false;
|
||||
refreshData(); // Reload list
|
||||
} else {
|
||||
throw new Error("API Fehler (Status: " + res.status + ")");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Model-Downloader] Archive error:", e);
|
||||
btn.disabled = false;
|
||||
status.textContent = "";
|
||||
if (e.message.includes("fetch") || e.name === "TypeError") {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
showConnectionError();
|
||||
} else {
|
||||
alert("Fehler: " + e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const addFAB = () => {
|
||||
if (document.getElementById("tki-downloader-fab")) return;
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "tki-downloader-fab";
|
||||
fab.innerHTML = "🚀";
|
||||
fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(255,204,0,0.5); border: 2px solid white; transition: transform 0.2s;";
|
||||
fab.onclick = sendWorkflow;
|
||||
document.body.appendChild(fab);
|
||||
};
|
||||
|
||||
const addV2Action = () => {
|
||||
if (app?.ui?.menu?.addPrimaryAction) {
|
||||
app.ui.menu.addPrimaryAction({ id: "tki.model-downloader.send", icon: "pi pi-download", label: "Send to Downloader", callback: sendWorkflow });
|
||||
app.ui.menu.addPrimaryAction({ id: "tki.model-downloader.cleanup", icon: "pi pi-trash", label: "Aufräumen / Archivieren", callback: openCleanupDialog });
|
||||
}
|
||||
};
|
||||
|
||||
const addClassic = () => {
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
if (menu) {
|
||||
if (!document.getElementById("tki-downloader-classic")) {
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "tki-downloader-classic";
|
||||
btn.textContent = "🚀 Downloader";
|
||||
btn.style.background = "#ffcc00"; btn.style.color = "white";
|
||||
btn.onclick = sendWorkflow;
|
||||
menu.appendChild(btn);
|
||||
}
|
||||
if (!document.getElementById("tki-cleanup-classic")) {
|
||||
const cleanBtn = document.createElement("button");
|
||||
cleanBtn.id = "tki-cleanup-classic";
|
||||
cleanBtn.textContent = "🧹 Aufräumen";
|
||||
cleanBtn.style.background = "#444"; cleanBtn.style.color = "white"; cleanBtn.style.marginLeft = "5px";
|
||||
cleanBtn.onclick = openCleanupDialog;
|
||||
menu.appendChild(cleanBtn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addFAB(); addClassic(); addV2Action();
|
||||
addUI();
|
||||
};
|
||||
|
||||
// NEU: Wir laden die API
|
||||
const setupListener = async () => {
|
||||
try {
|
||||
const { api } = await import("../../scripts/api.js");
|
||||
api.addEventListener("kippster-refresh-ui", (event) => {
|
||||
console.log("%c[Model-Downloader] Triggering UI Refresh!", "background: green; color: white;");
|
||||
|
||||
if (app && app.refresh) {
|
||||
app.refresh(); // Magischer Reload-Befehl
|
||||
|
||||
// Kleines Popup für den User
|
||||
const notify = (msg) => {
|
||||
if (app.ui?.dialog?.show) app.ui.dialog.show(msg);
|
||||
else if (window.comfyAPI?.dialog?.show) window.comfyAPI.dialog.show(msg);
|
||||
else alert(msg);
|
||||
};
|
||||
notify("✅ Downloads abgeschlossen! Modelle sind jetzt in ComfyUI verfügbar.");
|
||||
}
|
||||
});
|
||||
console.log("[Model-Downloader] WebSocket Listener aktiv.");
|
||||
} catch (e) {
|
||||
console.error("[Model-Downloader] Konnte API nicht für Listener laden.", e);
|
||||
const register = () => {
|
||||
if (window.app && window.app.registerExtension) {
|
||||
window.app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } });
|
||||
} else {
|
||||
if (document.readyState === "complete") initializeExtension();
|
||||
else window.addEventListener("load", initializeExtension);
|
||||
}
|
||||
};
|
||||
|
||||
setupListener();
|
||||
|
||||
if (app) {
|
||||
app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } });
|
||||
} else if (document.readyState === "complete") {
|
||||
initializeExtension();
|
||||
} else {
|
||||
window.addEventListener("load", initializeExtension);
|
||||
}
|
||||
register();
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package de.tki.comfymodels.service;
|
||||
|
||||
import de.tki.comfymodels.domain.ModelInfo;
|
||||
import de.tki.comfymodels.service.impl.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ComprehensiveStatusFlowTest {
|
||||
|
||||
private ComfyModelAnalyzer analyzer;
|
||||
private ConfigService configService;
|
||||
private ArchiveService archiveService;
|
||||
private PathResolver pathResolver;
|
||||
private LocalModelScanner localScanner;
|
||||
|
||||
@TempDir
|
||||
Path tempBase;
|
||||
|
||||
Path modelsDir;
|
||||
Path archiveDir;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws IOException {
|
||||
modelsDir = tempBase.resolve("models");
|
||||
archiveDir = tempBase.resolve("archive");
|
||||
Files.createDirectories(modelsDir);
|
||||
Files.createDirectories(archiveDir);
|
||||
|
||||
pathResolver = new PathResolver();
|
||||
configService = new ConfigService(new EncryptionUtils(), pathResolver);
|
||||
configService.setModelsPath(modelsDir.toString());
|
||||
configService.setArchivePath(archiveDir.toString());
|
||||
|
||||
localScanner = new LocalModelScanner(configService);
|
||||
archiveService = new ArchiveService(configService, pathResolver);
|
||||
analyzer = new ComfyModelAnalyzer();
|
||||
|
||||
ReflectionTestUtils.setField(analyzer, "modelListService", new ModelListService());
|
||||
// ComfyModelAnalyzer doesn't have pathResolver/configService/archiveService fields anymore,
|
||||
// it uses modelListService and AI services.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullStatusLifecycle() throws IOException {
|
||||
String modelName = "test_model.safetensors";
|
||||
String workflow = "{\"nodes\":[{\"type\":\"CheckpointLoaderSimple\",\"widgets_values\":[\"" + modelName + "\"]}]}";
|
||||
|
||||
// 1. Initial State: Missing (Idle)
|
||||
List<ModelInfo> results = analyzer.analyze(workflow, "test.json");
|
||||
assertEquals(1, results.size());
|
||||
assertEquals("Idle", getStatus(results.get(0), false, false, 0));
|
||||
|
||||
// 2. State: Known Good (URL found)
|
||||
results.get(0).setUrl("http://example.com/model.safetensors");
|
||||
assertEquals("✅ Known Good", getStatus(results.get(0), false, false, 0));
|
||||
|
||||
// 3. State: Size Mismatch (Local file exists but size unknown/different)
|
||||
Path modelPath = modelsDir.resolve("checkpoints").resolve(modelName);
|
||||
Files.createDirectories(modelPath.getParent());
|
||||
Files.write(modelPath, new byte[1000]); // 1KB local file
|
||||
|
||||
results.get(0).setByteSize(2000); // 2KB remote size
|
||||
assertEquals("🔄 Size Mismatch", getStatus(results.get(0), true, false, 1000));
|
||||
|
||||
// 4. State: Already Exists (Size match)
|
||||
Files.write(modelPath, new byte[2000]); // Update to 2KB
|
||||
assertEquals("✅ Already exists", getStatus(results.get(0), true, false, 2000));
|
||||
|
||||
// 5. State: Found in Archive (Removed locally, moved to archive)
|
||||
Files.delete(modelPath);
|
||||
Path archivePath = archiveDir.resolve("checkpoints").resolve(modelName);
|
||||
Files.createDirectories(archivePath.getParent());
|
||||
Files.write(archivePath, new byte[2000]);
|
||||
|
||||
assertEquals("📦 Found in Archive", getStatus(results.get(0), false, true, 0));
|
||||
|
||||
// 6. State: Already exists (Archived copy found) - Exists in both
|
||||
Files.write(modelPath, new byte[2000]);
|
||||
assertEquals("✅ Already exists (Archived copy found)", getStatus(results.get(0), true, true, 2000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to simulate the status logic from Main.java analyzeJsonContent()
|
||||
*/
|
||||
private String getStatus(ModelInfo info, boolean existsLocally, boolean existsInArchive, long localSize) {
|
||||
boolean sizeMismatch = false;
|
||||
if (existsLocally && info.getByteSize() > 0) {
|
||||
if (localSize != info.getByteSize()) {
|
||||
sizeMismatch = true;
|
||||
existsLocally = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (existsLocally && existsInArchive) return "✅ Already exists (Archived copy found)";
|
||||
if (existsLocally) return "✅ Already exists";
|
||||
if (existsInArchive) return "📦 Found in Archive";
|
||||
if (sizeMismatch) return "🔄 Size Mismatch";
|
||||
|
||||
if (info.getUrl().equals("MISSING")) return "Idle";
|
||||
|
||||
return "✅ Known Good";
|
||||
}
|
||||
}
|
||||
+69
-272
@@ -1,108 +1,62 @@
|
||||
// Try different ways to get the ComfyUI app and api objects
|
||||
// ComfyUI Model Downloader - Bridge Extension
|
||||
let app = null;
|
||||
let api = null;
|
||||
|
||||
const tryImports = async () => {
|
||||
const paths = [
|
||||
"../../scripts/app.js",
|
||||
"/scripts/app.js",
|
||||
"../../../scripts/app.js"
|
||||
];
|
||||
const apiPaths = [
|
||||
"../../scripts/api.js",
|
||||
"/scripts/api.js",
|
||||
"../../../scripts/api.js"
|
||||
];
|
||||
const initializeExtension = async () => {
|
||||
console.log("[Model-Downloader] Initializing...");
|
||||
|
||||
// 1. Try to find ComfyUI app and api objects
|
||||
const paths = ["../../scripts/app.js", "/scripts/app.js", "../../../scripts/app.js"];
|
||||
const apiPaths = ["../../scripts/api.js", "/scripts/api.js", "../../../scripts/api.js"];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
const mod = await import(path);
|
||||
if (mod.app) {
|
||||
app = mod.app;
|
||||
console.log(`[Model-Downloader] Successfully imported app from ${path}`);
|
||||
break;
|
||||
}
|
||||
if (mod.app) { app = mod.app; break; }
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
for (const path of apiPaths) {
|
||||
try {
|
||||
const mod = await import(path);
|
||||
if (mod.api) {
|
||||
api = mod.api;
|
||||
console.log(`[Model-Downloader] Successfully imported api from ${path}`);
|
||||
break;
|
||||
}
|
||||
if (mod.api) { api = mod.api; break; }
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
await tryImports();
|
||||
|
||||
// NEU: Dynamischer Import für den WebSocket-Listener
|
||||
const setupListener = async () => {
|
||||
const apiPaths = [
|
||||
"../../scripts/api.js",
|
||||
"/scripts/api.js",
|
||||
"../../../scripts/api.js"
|
||||
];
|
||||
|
||||
for (const path of apiPaths) {
|
||||
try {
|
||||
const mod = await import(path);
|
||||
if (mod.api) {
|
||||
console.log("%c[Model-Downloader] WebSocket Listener erfolgreich verknüpft!", "color: #00ff00; font-weight: bold;");
|
||||
|
||||
mod.api.addEventListener("kippster-refresh-ui", (event) => {
|
||||
console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Lade UI neu...", "background: #ff0000; color: #fff; font-size: 14px;");
|
||||
|
||||
if (app && typeof app.refresh === 'function') {
|
||||
app.refresh(); // Aktualisiert die Model-Dropdowns!
|
||||
|
||||
// Kleines UI-Popup zur Bestätigung
|
||||
if (app.ui?.dialog?.show) {
|
||||
app.ui.dialog.show("✅ Downloads abgeschlossen! Modelle sind jetzt im Dropdown.");
|
||||
} else {
|
||||
alert("✅ Downloads abgeschlossen! Modelle aktualisiert.");
|
||||
}
|
||||
}
|
||||
});
|
||||
break; // Erfolgreich gefunden, Schleife abbrechen
|
||||
}
|
||||
} catch (e) {}
|
||||
if (!app) {
|
||||
console.warn("[Model-Downloader] Could not find ComfyUI app object. Retrying in 2s...");
|
||||
setTimeout(initializeExtension, 2000);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
await setupListener();
|
||||
// 2. Setup WebSocket Listener for Auto-Refresh
|
||||
if (api) {
|
||||
console.log("%c[Model-Downloader] WebSocket Listener verknüpft!", "color: #00ff00; font-weight: bold;");
|
||||
api.addEventListener("kippster-refresh-ui", (event) => {
|
||||
console.log("%c🔥 [Model-Downloader] SIGNAL EMPFANGEN! Lade UI neu...", "background: #ff0000; color: #fff; font-size: 14px;");
|
||||
if (app && typeof app.refresh === 'function') {
|
||||
app.refresh();
|
||||
const msg = "✅ Downloads abgeschlossen! Modelle sind jetzt verfügbar.";
|
||||
if (app.ui?.dialog?.show) app.ui.dialog.show(msg);
|
||||
else alert(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Define setup logic that can be called by extension or as fallback
|
||||
const initializeExtension = async () => {
|
||||
// 3. Load Token and setup UI components
|
||||
let apiToken = null;
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const baseUrl = new URL(".", import.meta.url).href;
|
||||
const configUrl = `${baseUrl}config.json?v=${Date.now()}`;
|
||||
console.log(`[Model-Downloader] Loading config from: ${configUrl}`);
|
||||
const response = await fetch(configUrl);
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
apiToken = config.token;
|
||||
if (apiToken) {
|
||||
const masked = apiToken.substring(0, 4) + "..." + apiToken.substring(apiToken.length - 4);
|
||||
console.log(`[Model-Downloader] Token loaded successfully: ${masked}`);
|
||||
} else {
|
||||
console.warn("[Model-Downloader] Token in config.json is empty!");
|
||||
}
|
||||
} else {
|
||||
console.error(`[Model-Downloader] Could not load config.json (Status: ${response.status})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Model-Downloader] Failed to load config.json:", e);
|
||||
}
|
||||
} catch (e) { console.error("[Model-Downloader] Config load error:", e); }
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
await loadConfig();
|
||||
|
||||
const notify = (msg) => {
|
||||
if (app?.ui?.dialog?.show) app.ui.dialog.show(msg);
|
||||
@@ -117,225 +71,68 @@ const initializeExtension = async () => {
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:50px; margin-bottom:15px;">🚀❌</div>
|
||||
<h2 style="margin-top:0; color:#ffcc00;">Downloader nicht erreichbar</h2>
|
||||
<p style="line-height:1.5;">Die Model Downloader App antwortet nicht. Bitte stelle sicher, dass:</p>
|
||||
<ul style="text-align:left; display:inline-block; margin-bottom:20px;">
|
||||
<li>Die Java-App gestartet wurde.</li>
|
||||
<li>Port 12345 nicht blockiert wird.</li>
|
||||
</ul>
|
||||
<p>Noch nicht installiert? Lade die App hier herunter:</p>
|
||||
<p style="line-height:1.5;">Die Model Downloader App antwortet nicht. Bitte stelle sicher, dass sie läuft.</p>
|
||||
<a href="https://sourceforge.net/projects/comfymodeldownloader/" target="_blank" style="display:block; background:#ffcc00; color:black; text-decoration:none; padding:10px; border-radius:5px; font-weight:bold; margin-bottom:20px;">📥 Download via SourceForge</a>
|
||||
<button id="tki-error-close" style="background:#444; color:white; border:none; padding:10px 20px; border-radius:5px; cursor:pointer;">Schließen</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
dialog.querySelector("#tki-error-close").onclick = () => {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
dialog.querySelector("#tki-error-close").onclick = () => { dialog.close(); document.body.removeChild(dialog); };
|
||||
};
|
||||
|
||||
const sendWorkflow = async () => {
|
||||
if (!app || !app.graph) return notify("❌ Fehler: ComfyUI Graph nicht bereit.");
|
||||
if (!app?.graph) return notify("❌ Fehler: ComfyUI Graph nicht bereit.");
|
||||
if (!apiToken) return notify("❌ Sicherheits-Fehler: Kein API-Token gefunden.");
|
||||
|
||||
const workflow = app.graph.serialize();
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:12345/import", {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiToken}`
|
||||
},
|
||||
method: "POST", mode: "cors",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` },
|
||||
body: JSON.stringify(workflow)
|
||||
});
|
||||
if (response.ok) notify("🚀 Workflow erfolgreich gesendet!");
|
||||
else notify("❌ Fehler: Downloader abgelehnt (Status: " + response.status + ")");
|
||||
} catch (e) {
|
||||
showConnectionError();
|
||||
} catch (e) { showConnectionError(); }
|
||||
};
|
||||
|
||||
// UI Buttons
|
||||
const addUI = () => {
|
||||
if (!document.getElementById("tki-downloader-fab")) {
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "tki-downloader-fab";
|
||||
fab.innerHTML = "🚀";
|
||||
fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(0,0,0,0.5); border: 2px solid white;";
|
||||
fab.onclick = sendWorkflow;
|
||||
document.body.appendChild(fab);
|
||||
}
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
if (menu && !document.getElementById("tki-downloader-classic")) {
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "tki-downloader-classic";
|
||||
btn.textContent = "🚀 Downloader";
|
||||
btn.style.background = "#ffcc00"; btn.style.color = "white";
|
||||
btn.onclick = sendWorkflow;
|
||||
menu.appendChild(btn);
|
||||
}
|
||||
};
|
||||
|
||||
const openCleanupDialog = async () => {
|
||||
if (!apiToken) return notify("❌ Sicherheits-Fehler: Kein API-Token gefunden.");
|
||||
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.id = "tki-cleanup-dialog";
|
||||
dialog.style = "background:#222; color:white; border:2px solid #ffcc00; border-radius:10px; padding:20px; width:800px; max-height:80vh; overflow:hidden; font-family:sans-serif; box-shadow:0 0 30px rgba(0,0,0,0.5); z-index:20001;";
|
||||
|
||||
dialog.innerHTML = `
|
||||
<div id="tki-cleanup-content">
|
||||
<h2 style="margin-top:0; color:#ffcc00;">🧹 Lokale Modelle aufräumen</h2>
|
||||
<div id="tki-cleanup-loading" style="text-align:center; padding:40px;">
|
||||
<div style="border:4px solid #f3f3f3; border-top:4px solid #ffcc00; border-radius:50%; width:30px; height:30px; animation:spin 1s linear infinite; margin:0 auto 10px;"></div>
|
||||
<span>Lade lokale Modelle...</span>
|
||||
</div>
|
||||
<div id="tki-cleanup-table-container" style="display:none;">
|
||||
<div style="max-height:50vh; overflow-y:auto; margin-bottom:20px; border:1px solid #444;">
|
||||
<table style="width:100%; border-collapse:collapse; text-align:left;">
|
||||
<thead style="position:sticky; top:0; background:#333;">
|
||||
<tr>
|
||||
<th style="padding:10px;"><input type="checkbox" id="tki-select-all"></th>
|
||||
<th style="padding:10px;">Name</th>
|
||||
<th style="padding:10px;">Typ</th>
|
||||
<th style="padding:10px;">Größe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tki-cleanup-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span id="tki-cleanup-status" style="font-style:italic; color:#aaa;"></span>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<button id="tki-cleanup-cancel" style="background:#444; color:white; border:none; padding:10px 20px; border-radius:5px; cursor:pointer;">Abbrechen</button>
|
||||
<button id="tki-cleanup-archive" style="background:#ffcc00; color:white; border:none; padding:10px 20px; border-radius:5px; cursor:pointer; font-weight:bold;">Ausgewählte archivieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
`;
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
|
||||
const refreshData = async () => {
|
||||
const loading = dialog.querySelector("#tki-cleanup-loading");
|
||||
const container = dialog.querySelector("#tki-cleanup-table-container");
|
||||
loading.style.display = "block";
|
||||
container.style.display = "none";
|
||||
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:12345/api/models/local", {
|
||||
headers: { "Authorization": `Bearer ${apiToken}` }
|
||||
});
|
||||
const models = await response.json();
|
||||
|
||||
const tbody = dialog.querySelector("#tki-cleanup-tbody");
|
||||
tbody.innerHTML = models.map(m => `
|
||||
<tr style="border-bottom:1px solid #333;">
|
||||
<td style="padding:10px;"><input type="checkbox" class="tki-model-select" data-path="${m.save_path}/${m.name}"></td>
|
||||
<td style="padding:10px;">${m.name}</td>
|
||||
<td style="padding:10px;">${m.type}</td>
|
||||
<td style="padding:10px;">${m.size}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
loading.style.display = "none";
|
||||
container.style.display = "block";
|
||||
|
||||
// Wire up select all
|
||||
const selectAll = dialog.querySelector("#tki-select-all");
|
||||
const checkboxes = dialog.querySelectorAll(".tki-model-select");
|
||||
selectAll.checked = false;
|
||||
selectAll.onchange = () => checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||||
|
||||
} catch (e) {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
showConnectionError();
|
||||
}
|
||||
};
|
||||
|
||||
refreshData();
|
||||
|
||||
dialog.querySelector("#tki-cleanup-cancel").onclick = () => {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
|
||||
dialog.querySelector("#tki-cleanup-archive").onclick = async () => {
|
||||
const checkboxes = dialog.querySelectorAll(".tki-model-select");
|
||||
const selectedPaths = Array.from(checkboxes)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.getAttribute("data-path"));
|
||||
|
||||
if (selectedPaths.length === 0) return alert("Bitte wähle mindestens ein Modell aus.");
|
||||
|
||||
const status = dialog.querySelector("#tki-cleanup-status");
|
||||
const btn = dialog.querySelector("#tki-cleanup-archive");
|
||||
btn.disabled = true;
|
||||
status.textContent = `Archiviere ${selectedPaths.length} Dateien...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("http://127.0.0.1:12345/api/models/archive", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiToken}` },
|
||||
body: JSON.stringify(selectedPaths)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
alert(`Erfolgreich ${data.archived} Modelle archiviert!`);
|
||||
status.textContent = "";
|
||||
btn.disabled = false;
|
||||
refreshData(); // Reload list
|
||||
} else {
|
||||
throw new Error("API Fehler (Status: " + res.status + ")");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Model-Downloader] Archive error:", e);
|
||||
btn.disabled = false;
|
||||
status.textContent = "";
|
||||
if (e.message.includes("fetch") || e.name === "TypeError") {
|
||||
dialog.close();
|
||||
document.body.removeChild(dialog);
|
||||
showConnectionError();
|
||||
} else {
|
||||
alert("Fehler: " + e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const addFAB = () => {
|
||||
if (document.getElementById("tki-downloader-fab")) return;
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "tki-downloader-fab";
|
||||
fab.innerHTML = "🚀";
|
||||
fab.style = "position:fixed; bottom:30px; right:30px; z-index:10000; cursor:pointer; font-size:30px; background:#ffcc00; border-radius:50%; width:60px; height:60px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 20px rgba(255,204,0,0.5); border: 2px solid white; transition: transform 0.2s;";
|
||||
fab.onclick = sendWorkflow;
|
||||
document.body.appendChild(fab);
|
||||
};
|
||||
|
||||
const addV2Action = () => {
|
||||
if (app?.ui?.menu?.addPrimaryAction) {
|
||||
app.ui.menu.addPrimaryAction({ id: "tki.model-downloader.send", icon: "pi pi-download", label: "Send to Downloader", callback: sendWorkflow });
|
||||
app.ui.menu.addPrimaryAction({ id: "tki.model-downloader.cleanup", icon: "pi pi-trash", label: "Aufräumen / Archivieren", callback: openCleanupDialog });
|
||||
}
|
||||
};
|
||||
|
||||
const addClassic = () => {
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
if (menu) {
|
||||
if (!document.getElementById("tki-downloader-classic")) {
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "tki-downloader-classic";
|
||||
btn.textContent = "🚀 Downloader";
|
||||
btn.style.background = "#ffcc00"; btn.style.color = "white";
|
||||
btn.onclick = sendWorkflow;
|
||||
menu.appendChild(btn);
|
||||
}
|
||||
if (!document.getElementById("tki-cleanup-classic")) {
|
||||
const cleanBtn = document.createElement("button");
|
||||
cleanBtn.id = "tki-cleanup-classic";
|
||||
cleanBtn.textContent = "🧹 Aufräumen";
|
||||
cleanBtn.style.background = "#444"; cleanBtn.style.color = "white"; cleanBtn.style.marginLeft = "5px";
|
||||
cleanBtn.onclick = openCleanupDialog;
|
||||
menu.appendChild(cleanBtn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addFAB(); addClassic(); addV2Action();
|
||||
addUI();
|
||||
};
|
||||
|
||||
if (app) {
|
||||
app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } });
|
||||
} else if (document.readyState === "complete") {
|
||||
initializeExtension();
|
||||
} else {
|
||||
window.addEventListener("load", initializeExtension);
|
||||
}
|
||||
// Register the extension
|
||||
const register = () => {
|
||||
// We try multiple ways to hook into ComfyUI
|
||||
if (window.app && window.app.registerExtension) {
|
||||
window.app.registerExtension({ name: "TKI.ModelDownloader", async setup() { await initializeExtension(); } });
|
||||
} else {
|
||||
// Fallback for manual load
|
||||
if (document.readyState === "complete") initializeExtension();
|
||||
else window.addEventListener("load", initializeExtension);
|
||||
}
|
||||
};
|
||||
|
||||
register();
|
||||
|
||||
Reference in New Issue
Block a user