Compare commits
2
Commits
4920c4c0ba
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0f94c8811 | ||
|
|
16fba66723 |
+408
-42
@@ -229,10 +229,19 @@ async function runAgy(
|
||||
// Helper: estrarre il percorso immagine dall'output
|
||||
// ---------------------------------------------------------------------------
|
||||
function extractImagePath(text: string): string | undefined {
|
||||
const m =
|
||||
text.match(/IMAGE_PATH:\s*(\S+)/i) ||
|
||||
text.match(/(\/[^\s]+\.(?:png|jpe?g|webp|gif))/i);
|
||||
return m ? m[1] : undefined;
|
||||
// Candidati: prima la riga IMAGE_PATH, poi qualsiasi path immagine assoluto nel testo.
|
||||
// I placeholder ecoati dal prompt (es. "IMAGE_PATH: <percorso assoluto ...>") e i
|
||||
// path inesistenti vengono scartati: senza il filtro si ottenevano percorsi fasulli.
|
||||
const candidates: string[] = [];
|
||||
const m = text.match(/IMAGE_PATH:\s*(\S+)/i);
|
||||
if (m?.[1]) candidates.push(m[1]);
|
||||
for (const mm of text.matchAll(/(\/[^\s"'`]+?\.(?:png|jpe?g|webp|gif))/gi)) candidates.push(mm[1]);
|
||||
for (const candidate of candidates) {
|
||||
const clean = candidate.replace(/[.,;:)\]}>]+$/, "");
|
||||
if (!clean || clean.includes("<") || clean.includes(">")) continue;
|
||||
if (fs.existsSync(clean)) return clean;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -643,6 +652,121 @@ async function executeAgy(opts: AgyExecOptions) {
|
||||
const IMG_SUFFIX =
|
||||
"\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: <percorso assoluto dell'immagine generata>";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task immagine unificato: prima il protocollo diretto Antigravity (nessun
|
||||
// subprocess, ~7-25s), con fallback automatico al percorso agy CLI se il
|
||||
// modello non è disponibile o la risposta non contiene immagini.
|
||||
// ---------------------------------------------------------------------------
|
||||
const AGY_IMAGE_OUT_DIR = path.join(AGY_CHAT_DIR, "generated_images");
|
||||
|
||||
function antgSaveImages(images: AntgInlineImage[], outputDir: string | undefined, prefix: string): string[] {
|
||||
const dir = outputDir ?? AGY_IMAGE_OUT_DIR;
|
||||
const saved: string[] = [];
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
} catch {
|
||||
return saved;
|
||||
}
|
||||
const stamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
||||
images.forEach((img, i) => {
|
||||
const ext = img.mimeType.includes("jpeg") ? "jpg" : img.mimeType.includes("webp") ? "webp" : "png";
|
||||
const file = path.join(dir, `${prefix}_${stamp}_${i}.${ext}`);
|
||||
try {
|
||||
fs.writeFileSync(file, Buffer.from(img.data, "base64"));
|
||||
saved.push(file);
|
||||
} catch {
|
||||
/* ignora */
|
||||
}
|
||||
});
|
||||
return saved;
|
||||
}
|
||||
|
||||
async function agyImageTask(opts: {
|
||||
prompt: string; // prompt in stile CLI (può contenere i percorsi + IMG_SUFFIX)
|
||||
images?: string[];
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
model?: string;
|
||||
outputDir?: string;
|
||||
signal?: AbortSignal;
|
||||
label: string;
|
||||
}): Promise<{
|
||||
text: string;
|
||||
imagePaths: string[];
|
||||
engine: "direct" | "cli";
|
||||
usage?: any;
|
||||
exitCode?: number;
|
||||
directError?: string;
|
||||
}> {
|
||||
const imageModel = opts.model && /image/i.test(opts.model) ? opts.model : ANTG_IMAGE_MODEL_DEFAULT;
|
||||
let directError: string | undefined;
|
||||
const quotaBlocked = Date.now() < antgImageBlockedUntilMs;
|
||||
try {
|
||||
if (quotaBlocked) {
|
||||
const mins = Math.ceil((antgImageBlockedUntilMs - Date.now()) / 60000);
|
||||
throw new Error(`quota API immagine esaurita (reset stimato tra ~${mins} min): ${antgImageBlockedReason}`);
|
||||
}
|
||||
// Prompt per il modello: niente IMG_SUFFIX, percorsi → riferimenti allegati,
|
||||
// più un vincolo esplicito di composizione (migliora affidabilità su input).
|
||||
let prompt = opts.prompt.split(IMG_SUFFIX).join("").trim();
|
||||
for (const file of opts.images ?? []) prompt = prompt.split(file).join("(immagine allegata)");
|
||||
if (opts.images?.length) {
|
||||
prompt += "\nMantieni esattamente la composizione e l'aspect ratio dell'immagine di input; non ritagliare e non aggiungere testo.";
|
||||
}
|
||||
const res = await antgGenerateImages({
|
||||
prompt,
|
||||
images: opts.images,
|
||||
aspectRatio: opts.aspectRatio,
|
||||
imageSize: opts.imageSize,
|
||||
model: imageModel,
|
||||
signal: opts.signal,
|
||||
});
|
||||
if (!res.images.length) {
|
||||
throw new Error(
|
||||
res.finishMessage
|
||||
? `il modello non ha prodotto immagini: ${res.finishMessage.slice(0, 200)}`
|
||||
: `nessuna immagine nella risposta (finishReason=${res.finishReason ?? "?"})`,
|
||||
);
|
||||
}
|
||||
const imagePaths = antgSaveImages(res.images, opts.outputDir, opts.label.replace(/[^a-z0-9]+/gi, "_"));
|
||||
const outTok = res.usage?.candidatesTokenCount;
|
||||
const text =
|
||||
(res.text ? `${res.text}\n\n` : "") +
|
||||
`✅ ${opts.label}: ${imagePaths.length} immagine/i via API diretta Antigravity (${res.model}${outTok ? `, ${outTok} token output` : ""}).` +
|
||||
(imagePaths.length ? `\n${imagePaths.map((f) => `- ${f}`).join("\n")}` : "");
|
||||
return { text, imagePaths, engine: "direct", usage: res.usage };
|
||||
} catch (e: any) {
|
||||
directError = e?.message ?? String(e);
|
||||
}
|
||||
|
||||
// Fallback: percorso CLI agy (comportamento precedente all'integrazione diretta)
|
||||
const res = await executeAgy({
|
||||
prompt: opts.prompt,
|
||||
mode: "image",
|
||||
model: opts.model,
|
||||
stateless: true,
|
||||
filePaths: opts.images,
|
||||
outputDir: opts.outputDir,
|
||||
yolo: true,
|
||||
signal: opts.signal,
|
||||
});
|
||||
// Il path deve esistere davvero: se il CLI fallisce (es. quota) l'output può
|
||||
// contenere il placeholder del prompt, che non è un'immagine.
|
||||
const cliPath = res.imagePath && fs.existsSync(res.imagePath) ? res.imagePath : undefined;
|
||||
const note = cliPath
|
||||
? `\n\nℹ️ Fallback CLI agy (percorso diretto non disponibile): ${directError ?? "motivo non specificato"}`
|
||||
: `\n\n⚠️ Nessuna immagine prodotta. API diretta: ${directError ?? "errore non specificato"}${
|
||||
res.exitCode ? ` | CLI agy exitCode ${res.exitCode}` : ""
|
||||
}`;
|
||||
return {
|
||||
text: res.text + note,
|
||||
imagePaths: cliPath ? [cliPath] : [],
|
||||
engine: "cli",
|
||||
exitCode: res.exitCode,
|
||||
directError,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registrazione microfono (F12) e trascrizione
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1329,6 +1453,162 @@ async function antgLoadCodeAssist(token: string, base: string): Promise<string>
|
||||
return String(pid);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generazione immagini via protocollo diretto Antigravity (stesso token OAuth
|
||||
// del canale account). Validato con probe il 13/09/2026:
|
||||
// POST v1internal:generateContent (NON streaming), requestType "image_gen",
|
||||
// responseModalities ["IMAGE"], imageConfig {aspectRatio, imageSize};
|
||||
// immagine in candidates[0].content.parts[].inlineData (base64, image/jpeg).
|
||||
// I parts con thought=true sono immagini intermedie di ragionamento: ignorarli.
|
||||
// HTTP 200 può non contenere immagini: in tal caso candidate.finishMessage
|
||||
// spiega il rifiuto (nessun addebito) → si ricade sul percorso CLI.
|
||||
// gemini-3-pro-image NON è disponibile su questo account (HTTP 404).
|
||||
// ---------------------------------------------------------------------------
|
||||
const ANTG_IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image"; // Nano Banana 2
|
||||
const ANTG_IMAGE_MAX_OUTPUT_TOKENS = 32768; // 4K richiede ~2.9K token di output
|
||||
|
||||
// Quota immagine: l'API diretta risponde 429 RESOURCE_EXHAUSTED con
|
||||
// "quota will reset after XhYmZs" quando la capacità del modello è satura.
|
||||
// Finché il blocco è attivo si va diretti al percorso CLI, evitando round-trip
|
||||
// inutili.
|
||||
let antgImageBlockedUntilMs = 0;
|
||||
let antgImageBlockedReason = "";
|
||||
|
||||
function antgParseQuotaDelayMs(message: string): number {
|
||||
const m = message.match(/reset after\s*(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i);
|
||||
if (!m) return 0;
|
||||
const h = Number(m[1] ?? 0);
|
||||
const min = Number(m[2] ?? 0);
|
||||
const sec = Number(m[3] ?? 0);
|
||||
return ((h * 60 + min) * 60 + sec) * 1000;
|
||||
}
|
||||
|
||||
interface AntgInlineImage {
|
||||
mimeType: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface AntgImageCallResult {
|
||||
images: AntgInlineImage[];
|
||||
text: string;
|
||||
finishReason?: string;
|
||||
finishMessage?: string;
|
||||
usage?: any;
|
||||
model: string;
|
||||
}
|
||||
|
||||
function antgMimeFromPath(file: string): string {
|
||||
switch (path.extname(file).toLowerCase()) {
|
||||
case ".png":
|
||||
return "image/png";
|
||||
case ".webp":
|
||||
return "image/webp";
|
||||
case ".gif":
|
||||
return "image/gif";
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
return "image/jpeg";
|
||||
default:
|
||||
return "image/png";
|
||||
}
|
||||
}
|
||||
|
||||
async function antgGenerateImages(opts: {
|
||||
prompt: string;
|
||||
images?: string[];
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
model?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<AntgImageCallResult> {
|
||||
const model = opts.model ?? ANTG_IMAGE_MODEL_DEFAULT;
|
||||
const { pid, base } = await antgGetProject();
|
||||
const parts: any[] = [];
|
||||
for (const file of opts.images ?? []) {
|
||||
try {
|
||||
parts.push({
|
||||
inlineData: { mimeType: antgMimeFromPath(file), data: fs.readFileSync(file).toString("base64") },
|
||||
});
|
||||
} catch (e: any) {
|
||||
throw new Error(`Impossibile leggere l'immagine ${file}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
parts.push({ text: opts.prompt });
|
||||
|
||||
const generationConfig: any = {
|
||||
temperature: 1.0,
|
||||
topP: 0.95,
|
||||
topK: 40,
|
||||
maxOutputTokens: ANTG_IMAGE_MAX_OUTPUT_TOKENS,
|
||||
responseModalities: ["IMAGE"], // 1 sola immagine (TEXT+IMAGE può duplicarla)
|
||||
};
|
||||
const imageConfig: any = {};
|
||||
if (opts.aspectRatio) imageConfig.aspectRatio = opts.aspectRatio;
|
||||
if (opts.imageSize) imageConfig.imageSize = opts.imageSize;
|
||||
if (Object.keys(imageConfig).length) generationConfig.imageConfig = imageConfig;
|
||||
|
||||
const envelope = {
|
||||
project: pid,
|
||||
model,
|
||||
requestType: "image_gen",
|
||||
userAgent: "antigravity",
|
||||
requestId: `agent-${crypto.randomUUID().replace(/-/g, "")}`,
|
||||
request: { contents: [{ role: "user", parts }], generationConfig },
|
||||
};
|
||||
|
||||
const call = (tok: string) =>
|
||||
fetch(`${base}/v1internal:generateContent`, {
|
||||
method: "POST",
|
||||
headers: antgHeaders(tok),
|
||||
body: JSON.stringify(envelope),
|
||||
signal: antgAbortSignal(opts.signal, 300_000),
|
||||
});
|
||||
let resp = await call(await antgGetAccessToken());
|
||||
if (resp.status === 401 || resp.status === 403) {
|
||||
antgToken = null;
|
||||
resp = await call(await antgGetAccessToken());
|
||||
}
|
||||
const raw = await resp.text();
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 429 || /QUOTA_EXHAUSTED|RESOURCE_EXHAUSTED/.test(raw)) {
|
||||
const delay = antgParseQuotaDelayMs(raw);
|
||||
antgImageBlockedUntilMs = Date.now() + (delay || 30 * 60 * 1000);
|
||||
antgImageBlockedReason = raw.replace(/\s+/g, " ").slice(0, 200);
|
||||
}
|
||||
throw new Error(`Antigravity image HTTP ${resp.status}: ${raw.slice(0, 300)}`);
|
||||
}
|
||||
|
||||
let payload: any;
|
||||
try {
|
||||
payload = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error(`Risposta immagine non JSON: ${raw.slice(0, 200)}`);
|
||||
}
|
||||
const gem = payload?.response ?? payload;
|
||||
const candidate = gem?.candidates?.[0];
|
||||
const responseParts: any[] = candidate?.content?.parts ?? [];
|
||||
const images: AntgInlineImage[] = [];
|
||||
const texts: string[] = [];
|
||||
for (const part of responseParts) {
|
||||
const inline = part?.inlineData ?? part?.inline_data;
|
||||
if (inline?.data) {
|
||||
if (part.thought === true) continue; // immagine intermedia di ragionamento
|
||||
images.push({ mimeType: inline.mimeType ?? inline.mime_type ?? "image/png", data: inline.data });
|
||||
} else if (typeof part?.text === "string" && part.thought !== true) {
|
||||
texts.push(part.text);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
images,
|
||||
text: texts.join("\n").trim(),
|
||||
finishReason: candidate?.finishReason,
|
||||
finishMessage: candidate?.finishMessage,
|
||||
usage: gem?.usageMetadata,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
async function antgGetProject(): Promise<{ pid: string; base: string }> {
|
||||
if (antgProject) return antgProject;
|
||||
const token = await antgGetAccessToken();
|
||||
@@ -1598,9 +1878,75 @@ const ANTG_PROVIDER_MODELS: AntgProviderModelDef[] = [
|
||||
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)", reasoning: false, images: false, contextWindow: 262144, maxTokens: 32768 },
|
||||
];
|
||||
|
||||
// Modelli attualmente registrati nel provider (statici all'avvio, aggiornabili
|
||||
// con /agy:refresh-models dal catalogo vivo fetchAvailableModels).
|
||||
let antgRegisteredModels: AntgProviderModelDef[] = ANTG_PROVIDER_MODELS;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache persistente dei modelli (~/.config/agy-pi/models-cache.json)
|
||||
// Il catalogo vivo scaricato da /agy:refresh-models viene salvato qui, così la
|
||||
// lista sopravvive al riavvio dell'estensione. Se il file è assente, illeggibile
|
||||
// o corrotto si riparte da ANTG_PROVIDER_MODELS senza errori fatali.
|
||||
// ---------------------------------------------------------------------------
|
||||
const MODELS_CACHE_FILE = path.join(CONFIG_DIR, "models-cache.json");
|
||||
const MODELS_CACHE_VERSION = 1;
|
||||
|
||||
interface AntgModelsCache {
|
||||
version: number;
|
||||
refreshedAt: string;
|
||||
project?: string;
|
||||
models: AntgProviderModelDef[];
|
||||
}
|
||||
|
||||
function isAntgModelDef(value: any): value is AntgProviderModelDef {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
typeof value.id === "string" &&
|
||||
value.id.trim() !== "" &&
|
||||
typeof value.contextWindow === "number" &&
|
||||
value.contextWindow > 0 &&
|
||||
typeof value.maxTokens === "number" &&
|
||||
value.maxTokens > 0
|
||||
);
|
||||
}
|
||||
|
||||
function loadModelsCache(): AntgModelsCache | null {
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf8"));
|
||||
if (raw?.version !== MODELS_CACHE_VERSION) return null;
|
||||
if (!Array.isArray(raw.models) || raw.models.length === 0) return null;
|
||||
if (!raw.models.every(isAntgModelDef)) return null;
|
||||
return raw as AntgModelsCache;
|
||||
} catch {
|
||||
return null; // assente, illeggibile o corrotto → default
|
||||
}
|
||||
}
|
||||
|
||||
function saveModelsCache(models: AntgProviderModelDef[], projectId?: string) {
|
||||
try {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
const tmp = `${MODELS_CACHE_FILE}.tmp`;
|
||||
fs.writeFileSync(
|
||||
tmp,
|
||||
JSON.stringify(
|
||||
{
|
||||
version: MODELS_CACHE_VERSION,
|
||||
refreshedAt: new Date().toISOString(),
|
||||
project: projectId,
|
||||
models,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
fs.renameSync(tmp, MODELS_CACHE_FILE); // scrittura atomica
|
||||
} catch {
|
||||
/* ignora */
|
||||
}
|
||||
}
|
||||
|
||||
// Modelli attualmente registrati nel provider: cache persistente se presente,
|
||||
// altrimenti i default statici. Aggiornabili con /agy:refresh-models (che ora
|
||||
// salva su disco) e ripristinabili con /agy:models-reset.
|
||||
let antgRegisteredModels: AntgProviderModelDef[] = loadModelsCache()?.models ?? ANTG_PROVIDER_MODELS;
|
||||
|
||||
// Configurazione del provider "antigravity" (riusata all'avvio e al refresh).
|
||||
function antgProviderConfig(models: AntgProviderModelDef[]) {
|
||||
@@ -2056,6 +2402,9 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
text: Type.Optional(Type.String({ description: "Testo da includere nell'immagine (tra virgolette)." })),
|
||||
negative: Type.Optional(Type.String({ description: "Cosa evitare, in framing positivo (es. 'nessun testo')." })),
|
||||
model: Type.Optional(Type.String({ description: "Modello agy." })),
|
||||
imageSize: Type.Optional(
|
||||
Type.String({ description: 'Risoluzione immagine via API diretta: "512", "1K", "2K", "4K" (default 1K).' }),
|
||||
),
|
||||
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine generata." })),
|
||||
}),
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
@@ -2072,17 +2421,18 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
const prompt = parts.join(". ") + IMG_SUFFIX;
|
||||
|
||||
toolUpdate(onUpdate, "agy_generate: generazione immagine...");
|
||||
const res = await executeAgy({
|
||||
const res = await agyImageTask({
|
||||
prompt,
|
||||
mode: "image",
|
||||
aspectRatio: p.aspectRatio,
|
||||
imageSize: p.imageSize ?? "1K",
|
||||
model: p.model,
|
||||
stateless: true,
|
||||
outputDir: p.outputDir,
|
||||
signal,
|
||||
label: "agy_generate",
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: res.text }],
|
||||
details: { imagePath: res.imagePath, exitCode: res.exitCode },
|
||||
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2119,19 +2469,17 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
prompt += IMG_SUFFIX;
|
||||
|
||||
toolUpdate(onUpdate, "agy_edit: modifica immagine...");
|
||||
const res = await executeAgy({
|
||||
const res = await agyImageTask({
|
||||
prompt,
|
||||
mode: "image",
|
||||
images: [p.baseImage],
|
||||
model: p.model,
|
||||
stateless: true,
|
||||
filePaths: [p.baseImage],
|
||||
outputDir: p.outputDir,
|
||||
yolo: true,
|
||||
signal,
|
||||
label: "agy_edit",
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: res.text }],
|
||||
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
|
||||
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, baseImage: p.baseImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2164,19 +2512,17 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
IMG_SUFFIX;
|
||||
|
||||
toolUpdate(onUpdate, "agy_inpaint: modifica zona specifica...");
|
||||
const res = await executeAgy({
|
||||
const res = await agyImageTask({
|
||||
prompt,
|
||||
mode: "image",
|
||||
images: [p.baseImage],
|
||||
model: p.model,
|
||||
stateless: true,
|
||||
filePaths: [p.baseImage],
|
||||
outputDir: p.outputDir,
|
||||
yolo: true,
|
||||
signal,
|
||||
label: "agy_inpaint",
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: res.text }],
|
||||
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
|
||||
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, baseImage: p.baseImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2208,19 +2554,17 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
IMG_SUFFIX;
|
||||
|
||||
toolUpdate(onUpdate, "agy_style_transfer: applica stile...");
|
||||
const res = await executeAgy({
|
||||
const res = await agyImageTask({
|
||||
prompt,
|
||||
mode: "image",
|
||||
images: [p.baseImage],
|
||||
model: p.model,
|
||||
stateless: true,
|
||||
filePaths: [p.baseImage],
|
||||
outputDir: p.outputDir,
|
||||
yolo: true,
|
||||
signal,
|
||||
label: "agy_style_transfer",
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: res.text }],
|
||||
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
|
||||
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, baseImage: p.baseImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2253,19 +2597,17 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
IMG_SUFFIX;
|
||||
|
||||
toolUpdate(onUpdate, "agy_compose: combina immagini...");
|
||||
const res = await executeAgy({
|
||||
const res = await agyImageTask({
|
||||
prompt,
|
||||
mode: "image",
|
||||
images: p.images,
|
||||
model: p.model,
|
||||
stateless: true,
|
||||
filePaths: p.images,
|
||||
outputDir: p.outputDir,
|
||||
yolo: true,
|
||||
signal,
|
||||
label: "agy_compose",
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: res.text }],
|
||||
details: { imagePath: res.imagePath, images: p.images, exitCode: res.exitCode },
|
||||
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, images: p.images, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2296,19 +2638,17 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
IMG_SUFFIX;
|
||||
|
||||
toolUpdate(onUpdate, "agy_character: mantieni consistenza personaggio...");
|
||||
const res = await executeAgy({
|
||||
const res = await agyImageTask({
|
||||
prompt,
|
||||
mode: "image",
|
||||
images: [p.referenceImage],
|
||||
model: p.model,
|
||||
stateless: true,
|
||||
filePaths: [p.referenceImage],
|
||||
outputDir: p.outputDir,
|
||||
yolo: true,
|
||||
signal,
|
||||
label: "agy_character",
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: res.text }],
|
||||
details: { imagePath: res.imagePath, referenceImage: p.referenceImage, exitCode: res.exitCode },
|
||||
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, referenceImage: p.referenceImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2618,10 +2958,11 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
if (!models.length) throw new Error("Il catalogo non ha restituito modelli utilizzabili");
|
||||
antgRegisteredModels = models;
|
||||
pi.registerProvider("antigravity", antgProviderConfig(models));
|
||||
saveModelsCache(models, pid);
|
||||
ctx.ui.setStatus("agy:refresh-models", "");
|
||||
const lines = models.map((m) => ` ${m.id} — ${m.name}${m.reasoning ? " (thinking)" : ""}`).join("\n");
|
||||
ctx.ui.notify(
|
||||
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}`,
|
||||
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}\n\n💾 Lista salvata in ${MODELS_CACHE_FILE} (persistente al riavvio).`,
|
||||
"info",
|
||||
);
|
||||
} catch (e: any) {
|
||||
@@ -2631,6 +2972,31 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Comando: /agy:models-reset — rimuove la cache persistente dei modelli e
|
||||
// ripristina la lista statica di default.
|
||||
// =========================================================================
|
||||
pi.registerCommand("agy:models-reset", {
|
||||
description:
|
||||
"Rimuove la cache persistente dei modelli (~/.config/agy-pi/models-cache.json) e ri-registra il provider con la lista di default.",
|
||||
handler: async (_args, ctx) => {
|
||||
let removed = false;
|
||||
try {
|
||||
removed = fs.existsSync(MODELS_CACHE_FILE);
|
||||
fs.rmSync(MODELS_CACHE_FILE, { force: true });
|
||||
} catch (e: any) {
|
||||
ctx.ui.notify(`Errore rimozione cache modelli: ${e.message}`, "error");
|
||||
return;
|
||||
}
|
||||
antgRegisteredModels = ANTG_PROVIDER_MODELS;
|
||||
pi.registerProvider("antigravity", antgProviderConfig(ANTG_PROVIDER_MODELS));
|
||||
ctx.ui.notify(
|
||||
`♻️ ${removed ? "Cache rimossa" : "Nessuna cache presente"}: provider antigravity ripristinato a ${ANTG_PROVIDER_MODELS.length} modelli di default.`,
|
||||
"info",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Comandi interattivi
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user