mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-09 11:02:12 -03:00
fix(civitai): surface missing-model diagnostics and anchor image-page URL parsing
- Return the ResourceNotFoundError detail from get_civitai_versions and get_model_versions_status instead of a bare "Model not found", and let the download modal display the server-provided message - Anchor extract_civitai_image_id / extractCivitaiImageId to ^/images/<id> so API paths and partial numeric IDs are rejected, same as the model-page parsing fix in #1140
This commit is contained in:
@@ -2881,9 +2881,10 @@ class ModelLibraryHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await metadata_provider.get_model_versions(model_id)
|
response = await metadata_provider.get_model_versions(model_id)
|
||||||
except ResourceNotFoundError:
|
except ResourceNotFoundError as exc:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"success": False, "error": "Model not found"}, status=404
|
{"success": False, "error": str(exc) or "Model not found"},
|
||||||
|
status=404,
|
||||||
)
|
)
|
||||||
if not response or not response.get("modelVersions"):
|
if not response or not response.get("modelVersions"):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|||||||
@@ -2359,8 +2359,8 @@ class ModelCivitaiHandler:
|
|||||||
metadata_provider = await self._metadata_provider_factory()
|
metadata_provider = await self._metadata_provider_factory()
|
||||||
try:
|
try:
|
||||||
response = await metadata_provider.get_model_versions(model_id)
|
response = await metadata_provider.get_model_versions(model_id)
|
||||||
except ResourceNotFoundError:
|
except ResourceNotFoundError as exc:
|
||||||
return web.Response(status=404, text="Model not found")
|
return web.json_response({"error": str(exc)}, status=404)
|
||||||
if not response or not response.get("modelVersions"):
|
if not response or not response.get("modelVersions"):
|
||||||
return web.Response(status=404, text="Model not found")
|
return web.Response(status=404, text="Model not found")
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ def extract_civitai_image_id(url: str | None) -> str | None:
|
|||||||
if parsed is None:
|
if parsed is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
path_match = re.search(r"/images/(\d+)", parsed.path)
|
path_match = re.match(r"^/images/(\d+)(?:/|$)", parsed.path)
|
||||||
if not path_match:
|
if not path_match:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1310,7 +1310,7 @@ export class BaseModelApiClient {
|
|||||||
if (errorData && errorData.error && errorData.error.includes('Model type mismatch')) {
|
if (errorData && errorData.error && errorData.error.includes('Model type mismatch')) {
|
||||||
throw new Error(`This model is not a ${this.apiConfig.config.displayName}. Please switch to the appropriate page to download this model type.`);
|
throw new Error(`This model is not a ${this.apiConfig.config.displayName}. Please switch to the appropriate page to download this model type.`);
|
||||||
}
|
}
|
||||||
throw new Error('Failed to fetch model versions');
|
throw new Error((errorData && errorData.error) || 'Failed to fetch model versions');
|
||||||
}
|
}
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export function extractCivitaiImageId(url) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathMatch = parsedUrl.pathname.match(/\/images\/(\d+)/);
|
const pathMatch = parsedUrl.pathname.match(/^\/images\/(\d+)(?:\/|$)/);
|
||||||
return pathMatch ? pathMatch[1] : null;
|
return pathMatch ? pathMatch[1] : null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -379,6 +379,12 @@ describe('civitaiUtils', () => {
|
|||||||
it('rejects image-like URLs from unsupported hosts', () => {
|
it('rejects image-like URLs from unsupported hosts', () => {
|
||||||
expect(extractCivitaiImageId('https://example.com/images/126920345')).toBe(null);
|
expect(extractCivitaiImageId('https://example.com/images/126920345')).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(['civitai.com', 'civitai.red'])('rejects non-image pages on %s', (host) => {
|
||||||
|
for (const path of ['/api/v1/images/126920345', '/images/126920345invalid']) {
|
||||||
|
expect(extractCivitaiImageId(`https://${host}${path}`)).toBe(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('classifyModelRelinkUrl', () => {
|
describe('classifyModelRelinkUrl', () => {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from py.utils.session_logging import (
|
|||||||
)
|
)
|
||||||
from py.routes.misc_route_registrar import MISC_ROUTE_DEFINITIONS, MiscRouteRegistrar
|
from py.routes.misc_route_registrar import MISC_ROUTE_DEFINITIONS, MiscRouteRegistrar
|
||||||
from py.routes.misc_routes import MiscRoutes
|
from py.routes.misc_routes import MiscRoutes
|
||||||
|
from py.services.errors import ResourceNotFoundError
|
||||||
|
|
||||||
|
|
||||||
def _json_payload(response) -> dict[str, Any]:
|
def _json_payload(response) -> dict[str, Any]:
|
||||||
@@ -2748,6 +2749,38 @@ async def test_get_model_versions_status_supported_type_stays_interactive():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_model_versions_status_surfaces_not_found_details():
|
||||||
|
"""A missing CivitAI model keeps the provider's diagnostic message."""
|
||||||
|
|
||||||
|
class NotFoundProvider:
|
||||||
|
async def get_model_versions(self, _model_id):
|
||||||
|
raise ResourceNotFoundError("Civitai model 3390681 was not found")
|
||||||
|
|
||||||
|
async def metadata_factory():
|
||||||
|
return NotFoundProvider()
|
||||||
|
|
||||||
|
handler = ModelLibraryHandler(
|
||||||
|
ServiceRegistryAdapter(
|
||||||
|
get_lora_scanner=fake_scanner_factory,
|
||||||
|
get_checkpoint_scanner=fake_scanner_factory,
|
||||||
|
get_embedding_scanner=fake_scanner_factory,
|
||||||
|
get_other_scanner=fake_scanner_factory,
|
||||||
|
get_downloaded_version_history_service=fake_download_history_service_factory,
|
||||||
|
),
|
||||||
|
metadata_provider_factory=metadata_factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.get_model_versions_status(
|
||||||
|
FakeRequest(query={"modelId": "3390681"}) # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
payload = _json_payload(response)
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
assert payload["success"] is False
|
||||||
|
assert "3390681" in payload["error"]
|
||||||
|
|
||||||
|
|
||||||
class DummySidecarMigrationUseCase:
|
class DummySidecarMigrationUseCase:
|
||||||
def __init__(self, result):
|
def __init__(self, result):
|
||||||
self.result = result
|
self.result = result
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from py.routes.handlers.model_handlers import (
|
|||||||
ModelUpdateHandler,
|
ModelUpdateHandler,
|
||||||
)
|
)
|
||||||
from py.routes.model_route_registrar import COMMON_ROUTE_DEFINITIONS
|
from py.routes.model_route_registrar import COMMON_ROUTE_DEFINITIONS
|
||||||
|
from py.services.errors import ResourceNotFoundError
|
||||||
from py.services.service_registry import ServiceRegistry
|
from py.services.service_registry import ServiceRegistry
|
||||||
from py.utils.metadata_manager import MetadataManager
|
from py.utils.metadata_manager import MetadataManager
|
||||||
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
|
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
|
||||||
@@ -231,6 +232,43 @@ async def test_get_civitai_versions_degrades_when_download_history_unavailable(m
|
|||||||
assert payload[0]["hasBeenDownloaded"] is False
|
assert payload[0]["hasBeenDownloaded"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_civitai_versions_surfaces_not_found_details():
|
||||||
|
cache = SimpleNamespace(version_index={})
|
||||||
|
service = DummyService(cache)
|
||||||
|
|
||||||
|
class NotFoundProvider:
|
||||||
|
async def get_model_versions(self, model_id):
|
||||||
|
raise ResourceNotFoundError(f"Civitai model {model_id} was not found")
|
||||||
|
|
||||||
|
async def metadata_provider_factory():
|
||||||
|
return NotFoundProvider()
|
||||||
|
|
||||||
|
handler = ModelCivitaiHandler(
|
||||||
|
service=service,
|
||||||
|
settings_service=SimpleNamespace(get=lambda *_: False), # pyright: ignore[reportArgumentType]
|
||||||
|
ws_manager=SimpleNamespace(), # pyright: ignore[reportArgumentType]
|
||||||
|
logger=logging.getLogger(__name__),
|
||||||
|
metadata_provider_factory=metadata_provider_factory,
|
||||||
|
validate_model_type=lambda *_: True,
|
||||||
|
expected_model_types=lambda: "LoRA",
|
||||||
|
find_model_file=lambda *_: None,
|
||||||
|
metadata_sync=SimpleNamespace(), # pyright: ignore[reportArgumentType]
|
||||||
|
metadata_refresh_use_case=SimpleNamespace(), # pyright: ignore[reportArgumentType]
|
||||||
|
metadata_progress_callback=lambda *_args, **_kwargs: None, # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.get_civitai_versions(
|
||||||
|
SimpleNamespace(match_info={"model_id": "42"}) # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
text = response.text
|
||||||
|
assert text is not None
|
||||||
|
payload = json.loads(text)
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
assert "42" in payload["error"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_refresh_model_updates_filters_records_without_updates():
|
async def test_refresh_model_updates_filters_records_without_updates():
|
||||||
cache = SimpleNamespace(version_index={})
|
cache = SimpleNamespace(version_index={})
|
||||||
|
|||||||
@@ -10,14 +10,27 @@ from py.utils.civitai_utils import (
|
|||||||
resolve_license_payload,
|
resolve_license_payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("host", ["civitai.com", "civitai.red"])
|
@pytest.mark.parametrize("host", ["civitai.com", "civitai.red"])
|
||||||
@pytest.mark.parametrize("path", ["/api/download/models/3390681", "/api/v1/models/3390681", "/models/3390681invalid"])
|
@pytest.mark.parametrize(
|
||||||
|
"path",
|
||||||
|
["/api/download/models/3390681", "/api/v1/models/3390681", "/models/3390681invalid"],
|
||||||
|
)
|
||||||
def test_extract_civitai_model_url_parts_rejects_non_model_pages(host, path):
|
def test_extract_civitai_model_url_parts_rejects_non_model_pages(host, path):
|
||||||
assert extract_civitai_model_url_parts(
|
assert extract_civitai_model_url_parts(
|
||||||
f"https://{host}{path}?modelVersionId=3390681"
|
f"https://{host}{path}?modelVersionId=3390681"
|
||||||
) == (None, None)
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("host", ["civitai.com", "civitai.red"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path",
|
||||||
|
["/api/v1/images/126920345", "/images/126920345invalid"],
|
||||||
|
)
|
||||||
|
def test_extract_civitai_image_id_rejects_non_image_pages(host, path):
|
||||||
|
assert extract_civitai_image_id(f"https://{host}{path}") is None
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_license_payload_defaults():
|
def test_resolve_license_payload_defaults():
|
||||||
payload, flags = resolve_license_info({})
|
payload, flags = resolve_license_info({})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user