Bump Assistent to 0.6.0: board tabs, Cards Civitai fetch, and chat reliability.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 22:04:37 +03:00
co-authored by Cursor
parent c873894355
commit 46264d7227
9 changed files with 1591 additions and 324 deletions
+282 -42
View File
@@ -42,7 +42,7 @@ public class SwarmAssistentExtension : Extension
"catalog_card",
];
public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive"];
public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive", "cinema", "terse"];
const int MaxCivitaiHops = 2;
const int MaxLorasInInventory = 150;
@@ -61,7 +61,7 @@ public class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, personas, model cards, Generate loop, Civitai Confirm.";
License = "MIT";
Version = "0.5.3";
Version = "0.6.0";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
}
@@ -77,6 +77,8 @@ public class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
API.RegisterAPICall(AssistentGetTaste, false, PermUse);
API.RegisterAPICall(AssistentSaveTaste, true, PermUse);
API.RegisterAPICall(AssistentChat, true, PermUse);
API.RegisterAPICall(AssistentChatWS, true, PermUse);
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + personas + model cards)");
@@ -217,6 +219,8 @@ public class SwarmAssistentExtension : Extension
{
"lewd" => "Пошляк",
"aggressive" => "Агрессивный",
"cinema" => "Кинооператор",
"terse" => "Короткий",
_ => "Нейтральный",
},
["prompt"] = text,
@@ -247,11 +251,19 @@ public class SwarmAssistentExtension : Extension
{
continue;
}
string overlayPrompt = po["prompt"]?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(overlayPrompt) && byId.ContainsKey(id))
{
// Keep bundled prompt when overlay prompt is empty.
byId[id]["title"] = po["title"]?.ToString() ?? byId[id]["title"];
byId[id]["source"] = "overlay+bundled";
continue;
}
byId[id] = new JObject
{
["id"] = id,
["title"] = po["title"]?.ToString() ?? id,
["prompt"] = po["prompt"]?.ToString() ?? "",
["prompt"] = overlayPrompt,
["source"] = "overlay",
};
}
@@ -401,7 +413,8 @@ public class SwarmAssistentExtension : Extension
// Not installed — draft into wanted-cards + optionally enqueue download for next up.
Directory.CreateDirectory(WantedCardsDir());
string vid = card["version_id"]?.ToString() ?? "draft";
string rawVid = card["version_id"]?.ToString() ?? "draft";
string vid = Regex.IsMatch(rawVid, @"^\d+$") ? rawVid : "draft";
string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json");
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString()))
@@ -576,14 +589,56 @@ public class SwarmAssistentExtension : Extension
return sb.ToString();
}
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0)
string TasteJsonPath() => Path.Combine(DataRoot(), "Assistent", "taste.json");
public async Task<JObject> AssistentGetTaste(Session session)
{
// Pull Civitai sidecar next to weight + optional API version for examples.
await Task.CompletedTask;
string path = TasteJsonPath();
if (!File.Exists(path))
{
return new JObject { ["success"] = true, ["taste"] = null };
}
try
{
JObject taste = JObject.Parse(File.ReadAllText(path, Encoding.UTF8));
return new JObject { ["success"] = true, ["taste"] = taste };
}
catch (Exception ex)
{
return new JObject { ["error"] = $"taste.json: {ex.Message}" };
}
}
public async Task<JObject> AssistentSaveTaste(Session session, JObject taste)
{
await Task.CompletedTask;
if (taste is null)
{
return new JObject { ["error"] = "taste required" };
}
if (taste["updated"] == null)
{
taste["updated"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
string dir = Path.Combine(DataRoot(), "Assistent");
Directory.CreateDirectory(dir);
string path = TasteJsonPath();
File.WriteAllText(path, taste.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
return new JObject { ["success"] = true, ["path"] = path };
}
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0, bool fetch = false)
{
string set = SetNameForKind(kind);
string weight = ModelWeightPath(set, name);
JObject civitai = null;
JArray exampleUrls = [];
JArray previewUrls = [];
bool hasSidecar = false;
string fetchError = null;
bool fetched = false;
if (!string.IsNullOrWhiteSpace(weight))
{
string stem = Path.GetFileNameWithoutExtension(weight);
@@ -591,6 +646,7 @@ public class SwarmAssistentExtension : Extension
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
if (File.Exists(side))
{
hasSidecar = true;
try
{
civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8));
@@ -600,43 +656,113 @@ public class SwarmAssistentExtension : Extension
// ignore
}
}
foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" })
{
string prev = Path.Combine(dir ?? "", stem + suffix);
if (File.Exists(prev))
{
// Swarm View path — relative URL works in the same origin browser session.
previewUrls.Add($"View/Models/{(kind == "lora" ? "Lora" : "Stable-Diffusion")}/{Path.GetFileName(prev)}");
break;
}
}
}
if (civitai is not null)
{
if (version_id <= 0)
{
version_id = civitai["id"]?.Value<int?>() ?? 0;
}
if (civitai["images"] is JArray imgs)
{
foreach (JToken img in imgs.Take(3))
{
string u = img?["url"]?.ToString();
if (!string.IsNullOrWhiteSpace(u))
{
exampleUrls.Add(u);
}
}
}
if (civitai["trainedWords"] is null && civitai["model"] is JObject)
{
// keep as-is
}
CollectExampleUrls(civitai, exampleUrls);
}
JObject card = ReadCardObject(kind, name);
string hash = null;
string trigger = null;
try
{
if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h)
&& h.Models.TryGetValue(name, out T2IModel m))
&& (h.Models.TryGetValue(name, out T2IModel m)
|| h.Models.TryGetValue(name.Replace('\\', '/'), out m)))
{
trigger = m.Metadata?.TriggerPhrase;
hash = m.Metadata?.Hash;
}
}
catch
{
// ignore
}
if (fetch && civitai is null)
{
string apiKey = session.User.GetGenericData("civitai_api", "key") ?? "";
if (string.IsNullOrWhiteSpace(apiKey))
{
fetchError = "Civitai: нет ключа в User Settings";
}
else
{
try
{
JObject remote = null;
if (version_id > 0)
{
remote = await FetchCivitaiModelVersion(apiKey, version_id);
}
if (remote is null && !string.IsNullOrWhiteSpace(hash))
{
string sha = hash.Trim().ToLowerInvariant();
if (sha.StartsWith("sha256:"))
{
sha = sha["sha256:".Length..];
}
if (sha.Length == 64)
{
remote = await FetchCivitaiByHash(apiKey, sha);
}
else
{
fetchError ??= "Civitai: хеш модели не SHA256";
}
}
if (remote is not null)
{
civitai = remote;
fetched = true;
version_id = remote["id"]?.Value<int?>() ?? version_id;
CollectExampleUrls(remote, exampleUrls);
if (!string.IsNullOrWhiteSpace(weight))
{
try
{
string stem = Path.GetFileNameWithoutExtension(weight);
string dir = Path.GetDirectoryName(weight);
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
File.WriteAllText(side, remote.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
hasSidecar = true;
}
catch (Exception ex)
{
Logs.Debug($"AssistentGetCardMeta write sidecar: {ex.Message}");
}
}
}
else if (fetchError is null)
{
fetchError = string.IsNullOrWhiteSpace(hash)
? "Civitai: нет hash и version_id"
: "Хеш не найден на Civitai";
}
}
catch (Exception ex)
{
fetchError = $"Civitai: {ex.Message}";
}
}
}
JObject card = ReadCardObject(kind, name);
return new JObject
{
["success"] = true,
@@ -645,13 +771,124 @@ public class SwarmAssistentExtension : Extension
["version_id"] = version_id,
["trigger_phrase"] = trigger,
["has_card"] = card is not null,
["has_sidecar"] = hasSidecar,
["fetched"] = fetched,
["fetch_error"] = fetchError,
["card"] = card,
["civitai"] = civitai,
["example_urls"] = exampleUrls,
["preview_urls"] = previewUrls,
["weight_path"] = weight,
["hash"] = hash,
};
}
static void CollectExampleUrls(JObject civitai, JArray exampleUrls)
{
if (civitai?["images"] is not JArray imgs)
{
return;
}
foreach (JToken img in imgs.Take(6))
{
string u = img?["url"]?.ToString();
if (!string.IsNullOrWhiteSpace(u))
{
exampleUrls.Add(u);
}
}
}
async Task<JObject> FetchCivitaiByHash(string apiKey, string sha)
{
string[] hosts = ["civitai.red", "civitai.com"];
Exception last = null;
foreach (string host in hosts)
{
try
{
string url = $"https://{host}/api/v1/model-versions/by-hash/{sha}";
using HttpRequestMessage req = new(HttpMethod.Get, url);
if (!string.IsNullOrWhiteSpace(apiKey))
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
}
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
string body = await resp.Content.ReadAsStringAsync();
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
{
continue;
}
if (!resp.IsSuccessStatusCode)
{
last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
if ((int)resp.StatusCode is 401 or 403)
{
throw last;
}
continue;
}
return JObject.Parse(body);
}
catch (Exception ex) when (ex is not HttpRequestException && ex.Message.Contains("401"))
{
throw;
}
catch (Exception ex)
{
last = ex;
}
}
if (last is not null)
{
throw last;
}
return null;
}
async Task<JObject> FetchCivitaiModelVersion(string apiKey, int versionId)
{
string[] hosts = ["civitai.red", "civitai.com"];
Exception last = null;
foreach (string host in hosts)
{
try
{
string url = $"https://{host}/api/v1/model-versions/{versionId}";
using HttpRequestMessage req = new(HttpMethod.Get, url);
if (!string.IsNullOrWhiteSpace(apiKey))
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
}
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
string body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
{
last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
if ((int)resp.StatusCode is 401 or 403)
{
throw last;
}
continue;
}
return JObject.Parse(body);
}
catch (Exception ex)
{
last = ex;
if (ex.Message.Contains("401") || ex.Message.Contains("403"))
{
throw;
}
}
}
if (last is not null)
{
throw last;
}
return null;
}
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).
/// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).</summary>
public async Task<JObject> AssistentListInventory(Session session, bool rescan = false)
@@ -810,6 +1047,27 @@ public class SwarmAssistentExtension : Extension
["has_card"] = hasCard,
["krea_likely"] = LooksLikeKreaArch(model),
};
if (!string.IsNullOrWhiteSpace(weight))
{
string stem = Path.GetFileNameWithoutExtension(weight);
string dir = Path.GetDirectoryName(weight);
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
entry["has_sidecar"] = File.Exists(side);
foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" })
{
string prev = Path.Combine(dir ?? "", stem + suffix);
if (File.Exists(prev))
{
string folder = kind == "lora" ? "Lora" : "Stable-Diffusion";
entry["preview_url"] = $"View/Models/{folder}/{Path.GetFileName(prev)}";
break;
}
}
}
else
{
entry["has_sidecar"] = false;
}
if (triggers is not null && triggers.Count > 0)
{
entry["triggers"] = triggers;
@@ -1181,21 +1439,7 @@ public class SwarmAssistentExtension : Extension
{
return false;
}
if (!string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
{
return true;
}
if (patch["actions"] is JArray acts)
{
foreach (JToken a in acts)
{
if (string.Equals(a?.ToString(), "search_civitai", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
return !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch));
}
async Task<(string reply, JObject raw, JArray civitaiResults)> RunChatWithHops(
@@ -1228,10 +1472,6 @@ public class SwarmAssistentExtension : Extension
}
string query = ExtractSearchQuery(patch);
if (string.IsNullOrWhiteSpace(query))
{
query = userMessages.LastOrDefault(m => m["role"]?.ToString() == "user")?["content"]?.ToString() ?? "";
}
if (string.IsNullOrWhiteSpace(query))
{
break;
}