Add capture wanted and harden Assistent wanted-queue merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 21:29:00 +03:00
co-authored by Cursor
parent 08120194f9
commit 6c45a7560e
4 changed files with 78 additions and 8 deletions
+1
View File
@@ -79,6 +79,7 @@ gpu-rent up --yes --ollama
| `gpu-rent pull-output` | VM `Output/``./Output` | | `gpu-rent pull-output` | VM `Output/``./Output` |
| `gpu-rent capture` / `capture all` | Инвентарь VM → merge **ссылок** в `models.yaml` + `extensions.yaml` (веса не качать) | | `gpu-rent capture` / `capture all` | Инвентарь VM → merge **ссылок** в `models.yaml` + `extensions.yaml` (веса не качать) |
| `gpu-rent capture models` / `--kind lora` | Только модели (фильтр по типу) | | `gpu-rent capture models` / `--kind lora` | Только модели (фильтр по типу) |
| `gpu-rent capture wanted` | Очередь Assistent Cards → `models.yaml` |
| `gpu-rent capture extensions` | Только git Extensions / DLNodes | | `gpu-rent capture extensions` | Только git Extensions / DLNodes |
| `gpu-rent capture --dry-run` | Отчёт без записи файлов | | `gpu-rent capture --dry-run` | Отчёт без записи файлов |
| `gpu-rent resize-data --gb 400` | Data volume **только вверх** | | `gpu-rent resize-data --gb 400` | Data volume **только вверх** |
+2 -1
View File
@@ -23,11 +23,12 @@ copy models.example.yaml models.yaml
```powershell ```powershell
.\gpu-rent.ps1 capture models # merge Civitai url → models.yaml .\gpu-rent.ps1 capture models # merge Civitai url → models.yaml
.\gpu-rent.ps1 capture models --kind lora .\gpu-rent.ps1 capture models --kind lora
.\gpu-rent.ps1 capture wanted # только очередь Assistent Cards → models.yaml
.\gpu-rent.ps1 capture --dry-run # models + extensions, без записи .\gpu-rent.ps1 capture --dry-run # models + extensions, без записи
.\gpu-rent.ps1 capture all .\gpu-rent.ps1 capture all
``` ```
Берёт `{stem}.civitai.json` или SHA256 → Civitai `by-hash`. Также мержит очередь Assistent `/mnt/swarm_data/.gpu-rent-wanted-models.yaml` (модели, добавленные из вкладки Cards). Дубли `modelVersionId` не дублируются. Перед записью — `models.yaml.bak`. Неизвестные файлы (нет в Civitai) — только в отчёте. Берёт `{stem}.civitai.json` или SHA256 → Civitai `by-hash`. Также мержит очередь Assistent `/mnt/swarm_data/.gpu-rent-wanted-models.yaml` (модели, добавленные из вкладки Cards / Enqueue wanted) — на `capture models|wanted|all` и перед `seed-models` / `up`. Дубли `modelVersionId` не дублируются. Перед записью — `models.yaml.bak`. Неизвестные файлы (нет в Civitai) — только в отчёте.
Локальный SwarmUI на `7801` не зеркалируем. Только дерево `./Models` приложения. Локальный SwarmUI на `7801` не зеркалируем. Только дерево `./Models` приложения.
+59 -7
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
import re
import shutil import shutil
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -420,14 +421,15 @@ def merge_wanted_models_from_vm(cfg: Config, host: str, log: Log) -> int:
).strip() ).strip()
if not raw: if not raw:
return 0 return 0
try:
data = yaml.safe_load(raw) or {}
except Exception as exc:
log(f"wanted-models: bad yaml — {exc}")
return 0
if not isinstance(data, dict):
return 0
items: list[ModelCaptureItem] = [] items: list[ModelCaptureItem] = []
data: dict = {}
try:
parsed = yaml.safe_load(raw) or {}
if isinstance(parsed, dict):
data = parsed
except Exception as exc:
log(f"wanted-models: yaml parse — {exc}; fallback line scan")
for kind, rows in data.items(): for kind, rows in data.items():
if kind not in MODEL_DIRS or not isinstance(rows, list): if kind not in MODEL_DIRS or not isinstance(rows, list):
continue continue
@@ -451,6 +453,56 @@ def merge_wanted_models_from_vm(cfg: Config, host: str, log: Log) -> int:
source="wanted", source="wanted",
) )
) )
# Recover entries lost to duplicate YAML keys (old Assistent writers appended
# a fresh `lora:` block each time; PyYAML keeps only the last).
kind = "lora"
pending_url = None
pending_title = None
pending_vid = None
seen_urls = {i.url for i in items}
def flush_pending() -> None:
nonlocal pending_url, pending_title, pending_vid
if not pending_url or pending_url in seen_urls:
pending_url = pending_title = pending_vid = None
return
vid_i = pending_vid or extract_version_id(pending_url)
items.append(
ModelCaptureItem(
kind=kind if kind in MODEL_DIRS else "lora",
url=pending_url,
title=str(pending_title or Path(pending_url).name),
version_id=vid_i,
source="wanted",
)
)
seen_urls.add(pending_url)
pending_url = pending_title = pending_vid = None
for line in raw.splitlines():
t = line.strip()
m_kind = re.match(r"^([A-Za-z0-9_-]+):\s*$", t)
if m_kind and not t.startswith("-"):
flush_pending()
kind = m_kind.group(1).strip().lower()
continue
m_url = re.match(r"^-\s*url:\s*[\"']?(.+?)[\"']?\s*$", t)
if m_url:
flush_pending()
pending_url = m_url.group(1).strip()
continue
if not pending_url:
continue
m_title = re.match(r"^title:\s*[\"']?(.+?)[\"']?\s*$", t)
if m_title:
pending_title = m_title.group(1).strip()
continue
m_vid = re.match(r"^version_id:\s*(\d+)\s*$", t)
if m_vid:
pending_vid = int(m_vid.group(1))
flush_pending()
if not items: if not items:
return 0 return 0
added, skipped = merge_models_yaml(Path(cfg.models_manifest), items, dry_run=False) added, skipped = merge_models_yaml(Path(cfg.models_manifest), items, dry_run=False)
+16
View File
@@ -986,6 +986,22 @@ def capture_models_cmd(
_die(exc) _die(exc)
@capture_app.command("wanted")
def capture_wanted_cmd() -> None:
"""Только очередь Assistent (.gpu-rent-wanted-models.yaml) → models.yaml."""
try:
from gpu_rent.capture import merge_wanted_models_from_vm
cfg, host = _live()
n = merge_wanted_models_from_vm(cfg, host, log)
if n:
log(f"wanted→models.yaml: +{n}")
else:
log("wanted→models.yaml: пусто или уже есть")
except GpuRentError as exc:
_die(exc)
@capture_app.command("extensions") @capture_app.command("extensions")
def capture_extensions_cmd( def capture_extensions_cmd(
dry_run: bool = typer.Option(False, "--dry-run"), dry_run: bool = typer.Option(False, "--dry-run"),