feat(ollama): enhance model pull with improved error handling, persistence, and UI

- Add catalog metadata lookup for better error messages with hints (e.g., for missing manifests or tags)
- Persist downloaded model references in saved remote models for future reuse
- Replace searchRemoteModels import with getSavedRemoteModels in search route
- Enhance pull progress UI to display download bytes, digest, and fallback message when data is unavailable
- Improve error payload with model details, URL, and tags count to guide users toward valid tags or models
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-29 19:42:24 -03:00
1 parent 1545020afe
commit a8bb7cc22d
33 files changed
+1298 -235

No files matched your search

+2
View File
@@ -188,6 +188,8 @@ export async function searchRemoteModels(query?: string): Promise<any[]> {
return filtered.map((m) => ({
name: m.name,
description: m.description,
url: m.url,
tags_count: m.tags_count,
installed: installedNames.has(m.name),
remote: true,
}));
+23 -3
View File
@@ -127,8 +127,9 @@ export class MCPInstallerService {
const logs: string[] = [];
try {
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
const result = await spawnWithProgress(
"npm",
npmBin,
["install", packageName, "--prefix", npmDir],
{
cwd: npmDir,
@@ -171,15 +172,34 @@ export class MCPInstallerService {
// Construir ambiente
// Para NPM, executar npx do diretório onde foi instalado (não usa --prefix que não funciona)
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
const environment: MCPEnvironment = {
type: "npm",
path: npmDir,
executable: "npx",
args: [packageName],
executable: npxBin,
args: ["-y", packageName],
};
return environment;
} catch (error: any) {
const msg = String(error?.message || "");
if (msg.includes("spawn EINVAL") || msg.includes("Failed to spawn process") || msg.includes("ENOENT")) {
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
onProgress({
status: "installing" as MCPInstallationStatus,
message: "Falling back to npx runtime execution (no preinstall)",
percentage: 60,
logs: config.enableLogs ? [msg] : undefined,
});
const environment: MCPEnvironment = {
type: "npm",
path: npmDir,
executable: npxBin,
args: ["-y", packageName],
};
return environment;
}
throw new Error(`NPM installation failed: ${error.message}`);
}
}
+2
View File
@@ -46,6 +46,7 @@ export class MCPValidatorService {
cwd: environment.path, // Executar do diretório de instalação
env,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32", // Workaround para *.cmd no Windows
});
let stdout = "";
@@ -250,6 +251,7 @@ export class MCPValidatorService {
cwd: environment.path, // Executar do diretório de instalação
env,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
});
let hasResolved = false;
+62
View File
@@ -0,0 +1,62 @@
import fs from "node:fs/promises";
import path from "node:path";
export type SavedRemoteModel = {
name: string; // full ref, e.g., owner/model:tag or model:tag
description?: string;
url?: string;
tags_count?: number;
installed?: boolean;
lastUsedAt?: string; // ISO
};
type SavedRemoteFile = {
models: SavedRemoteModel[];
};
function getFilePath() {
return path.join(process.cwd(), "database", "user-remote-models.json");
}
async function ensureFile(): Promise<void> {
const file = getFilePath();
try {
await fs.access(file);
} catch {
const initial: SavedRemoteFile = { models: [] };
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, JSON.stringify(initial, null, 2), "utf-8");
}
}
export async function getSavedRemoteModels(): Promise<SavedRemoteModel[]> {
await ensureFile();
const raw = await fs.readFile(getFilePath(), "utf-8");
try {
const json = JSON.parse(raw) as SavedRemoteFile;
return Array.isArray(json.models) ? json.models : [];
} catch {
return [];
}
}
export async function addSavedRemoteModel(entry: SavedRemoteModel): Promise<void> {
await ensureFile();
const current = await getSavedRemoteModels();
const nowIso = new Date().toISOString();
const idx = current.findIndex((m) => m.name === entry.name);
const merged: SavedRemoteModel = {
...entry,
installed: entry.installed ?? false,
lastUsedAt: entry.lastUsedAt ?? nowIso,
};
if (idx >= 0) {
current[idx] = { ...current[idx], ...merged, lastUsedAt: nowIso };
} else {
current.unshift(merged);
}
const toWrite: SavedRemoteFile = { models: current.slice(0, 500) };
await fs.writeFile(getFilePath(), JSON.stringify(toWrite, null, 2), "utf-8");
}