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:
+282
-42
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user