fix: describe images relative to the ComfyUI tree in API responses
Gallery and prompt responses carried absolute server paths: /images/output entries (path, root_dir), thumbnails_path, the images/prompt lookup, every image list (image_path plus its normalised file_path duplicate) and the preview images attached to prompt listings. They now go through _public_path: /images/output 'path' is relative to its root (what the delete and autotag endpoints resolve against), 'root_dir' and every other image_path are relative to the ComfyUI base, and file_path is dropped. recent/all/search image lists also gain url/thumbnail_url via the same enrichment the prompt-images route already had. Remaining str(e) error bodies in both modules use the basename-only error text.
This commit is contained in:
+69
-29
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from aiohttp import web
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from .prompts import bad_request, parse_page_params
|
||||
from .prompts import bad_request, parse_page_params, publish_image_paths
|
||||
from .prompts import safe_error_message as _safe_error
|
||||
|
||||
# Only these file types are ever served by the image routes, regardless of
|
||||
@@ -34,6 +34,16 @@ IMAGE_EXTENSIONS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
def _public_path(path):
|
||||
"""Response-safe form of *path* (relative to the ComfyUI tree).
|
||||
|
||||
Imported lazily: the package defines it after importing this module.
|
||||
"""
|
||||
from . import _public_path as public_path
|
||||
|
||||
return public_path(path)
|
||||
|
||||
|
||||
def _canonical(path):
|
||||
"""Resolve symlinks and normalise case so paths compare on every OS."""
|
||||
return Path(os.path.normcase(os.path.realpath(str(path))))
|
||||
@@ -225,8 +235,14 @@ def _iter_media_files(output_path, max_files):
|
||||
yield root_path / name
|
||||
|
||||
|
||||
def _output_image_entry(media_path, output_path, root_index):
|
||||
"""Build one gallery entry for *media_path* (blocking stat)."""
|
||||
def _output_image_entry(media_path, output_path, root_index, public_root=None):
|
||||
"""Build one gallery entry for *media_path* (blocking stat).
|
||||
|
||||
``path`` is relative to its root (what delete/autotag resolve against) and
|
||||
``root_dir`` is the root's public form; neither reveals the server layout.
|
||||
"""
|
||||
if public_root is None:
|
||||
public_root = _public_path(output_path)
|
||||
stat = media_path.stat()
|
||||
rel_path = media_path.relative_to(output_path)
|
||||
extension = media_path.suffix.lower()
|
||||
@@ -245,9 +261,9 @@ def _output_image_entry(media_path, output_path, root_index):
|
||||
return {
|
||||
"id": hashlib.sha1(str(media_path).encode("utf-8")).hexdigest()[:16],
|
||||
"filename": media_path.name,
|
||||
"path": str(media_path),
|
||||
"path": rel_path.as_posix(),
|
||||
"relative_path": str(rel_path),
|
||||
"root_dir": str(output_path),
|
||||
"root_dir": public_root,
|
||||
"root_index": root_index,
|
||||
"url": f"/prompt_manager/images/serve/{rel_path.as_posix()}",
|
||||
"thumbnail_url": thumbnail_url,
|
||||
@@ -325,6 +341,11 @@ class ImageRoutesMixin:
|
||||
async def get_gallery_subfolders_route(request):
|
||||
return await self.get_gallery_subfolders(request)
|
||||
|
||||
def _present_images(self, images):
|
||||
"""Image dicts ready for a response: urls added, server paths hidden."""
|
||||
self._enrich_images(images)
|
||||
return publish_image_paths(images, _public_path)
|
||||
|
||||
async def get_prompt_images(self, request):
|
||||
"""Get all images for a specific prompt."""
|
||||
try:
|
||||
@@ -335,7 +356,7 @@ class ImageRoutesMixin:
|
||||
cleaned_images = [self._clean_nan_recursive(image) for image in images]
|
||||
|
||||
# Add url and thumbnail_url so the frontend can serve them
|
||||
self._enrich_images(cleaned_images)
|
||||
cleaned_images = self._present_images(cleaned_images)
|
||||
|
||||
# Additional fallback: convert to JSON string and clean NaN values manually
|
||||
try:
|
||||
@@ -361,7 +382,9 @@ class ImageRoutesMixin:
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Get prompt images error: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": _safe_error(e)}, status=500
|
||||
)
|
||||
|
||||
async def get_recent_images(self, request):
|
||||
"""Get recently generated images (bounded window, newest first)."""
|
||||
@@ -371,8 +394,8 @@ class ImageRoutesMixin:
|
||||
except ValueError:
|
||||
return bad_request("limit and offset must be integers")
|
||||
|
||||
images = await self._run_in_executor(
|
||||
self.db.get_recent_images, limit, offset
|
||||
images = self._present_images(
|
||||
await self._run_in_executor(self.db.get_recent_images, limit, offset)
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
@@ -404,8 +427,10 @@ class ImageRoutesMixin:
|
||||
except ValueError:
|
||||
return bad_request("limit and offset must be integers")
|
||||
|
||||
images = await self._run_in_executor(
|
||||
self.db.get_all_images, limit=limit, offset=offset
|
||||
images = self._present_images(
|
||||
await self._run_in_executor(
|
||||
self.db.get_all_images, limit=limit, offset=offset
|
||||
)
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
@@ -441,8 +466,10 @@ class ImageRoutesMixin:
|
||||
except ValueError:
|
||||
return bad_request("limit and offset must be integers")
|
||||
|
||||
images = await self._run_in_executor(
|
||||
self.db.search_images_by_prompt, query, limit, offset
|
||||
images = self._present_images(
|
||||
await self._run_in_executor(
|
||||
self.db.search_images_by_prompt, query, limit, offset
|
||||
)
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
@@ -668,7 +695,9 @@ class ImageRoutesMixin:
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Subfolders error: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": _safe_error(e)}, status=500
|
||||
)
|
||||
|
||||
# Fixed quality presets: clients choose a name, never a raw pixel size.
|
||||
THUMBNAIL_SIZES = {"low": (150, 150), "medium": (300, 300), "high": (600, 600)}
|
||||
@@ -722,7 +751,7 @@ class ImageRoutesMixin:
|
||||
)
|
||||
payload = _thumbnail_complete_payload(len(targets), stats, start)
|
||||
payload["success"] = True
|
||||
payload["thumbnails_path"] = str(thumbnails_dir)
|
||||
payload["thumbnails_path"] = _public_path(thumbnails_dir)
|
||||
self.logger.info(payload["message"])
|
||||
return payload
|
||||
|
||||
@@ -1109,7 +1138,9 @@ class ImageRoutesMixin:
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Clear thumbnails error: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": _safe_error(e)}, status=500
|
||||
)
|
||||
|
||||
async def link_image_to_prompt(self, request):
|
||||
"""Link a generated image (inside an allowed directory) to a prompt."""
|
||||
@@ -1175,17 +1206,15 @@ class ImageRoutesMixin:
|
||||
self.db.get_image_prompt_info, image_path
|
||||
)
|
||||
if prompt_data:
|
||||
prompt_data["image_path"] = image_path
|
||||
if prompt_data:
|
||||
prompt_data["image_path"] = _public_path(image_path)
|
||||
return web.json_response({"success": True, "prompt": prompt_data})
|
||||
else:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "No prompt found for this image",
|
||||
"image_path": image_path,
|
||||
}
|
||||
)
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "No prompt found for this image",
|
||||
"image_path": _public_path(image_path),
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as db_error:
|
||||
self.logger.error(f"Database error in get_image_prompt: {db_error}")
|
||||
@@ -1195,7 +1224,9 @@ class ImageRoutesMixin:
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Get image prompt error: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": _safe_error(e)}, status=500
|
||||
)
|
||||
|
||||
async def delete_image(self, request):
|
||||
"""Delete an image record."""
|
||||
@@ -1218,7 +1249,9 @@ class ImageRoutesMixin:
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Delete image error: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": _safe_error(e)}, status=500
|
||||
)
|
||||
|
||||
async def _collect_output_images(self, output_dirs, subfolder):
|
||||
"""Merge cached per-root listings, newest first, optionally filtered."""
|
||||
@@ -1237,9 +1270,16 @@ class ImageRoutesMixin:
|
||||
|
||||
def _format_output_page(self, page):
|
||||
entries = []
|
||||
public_roots = {}
|
||||
for media_path, _mtime, output_path, root_index in page:
|
||||
if output_path not in public_roots:
|
||||
public_roots[output_path] = _public_path(output_path)
|
||||
try:
|
||||
entries.append(_output_image_entry(media_path, output_path, root_index))
|
||||
entries.append(
|
||||
_output_image_entry(
|
||||
media_path, output_path, root_index, public_roots[output_path]
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing media {media_path.name}: {e}")
|
||||
return entries
|
||||
|
||||
+109
-27
@@ -42,6 +42,25 @@ def safe_error_message(exc):
|
||||
return str(exc) or exc.__class__.__name__
|
||||
|
||||
|
||||
def publish_image_paths(images, public_path):
|
||||
"""Image dicts for a response: ``image_path`` in its public (ComfyUI
|
||||
relative) form and the absolute ``file_path`` duplicate removed.
|
||||
|
||||
``url``/``thumbnail_url`` added by enrichment are left untouched, so the
|
||||
frontend keeps working without ever seeing the server's directory layout.
|
||||
"""
|
||||
published = []
|
||||
for image in images:
|
||||
if not isinstance(image, dict):
|
||||
published.append(image)
|
||||
continue
|
||||
image = {k: v for k, v in image.items() if k != "file_path"}
|
||||
if "image_path" in image:
|
||||
image["image_path"] = public_path(image["image_path"])
|
||||
published.append(image)
|
||||
return published
|
||||
|
||||
|
||||
try:
|
||||
from ...utils.validators import (
|
||||
validate_prompt_text,
|
||||
@@ -159,6 +178,15 @@ class PromptRoutesMixin:
|
||||
async def export_prompts_route(request):
|
||||
return await self.export_prompts(request)
|
||||
|
||||
def _present_prompts(self, prompts):
|
||||
"""Prompts ready for a response: image urls added, server paths hidden."""
|
||||
self._enrich_prompt_images(prompts)
|
||||
for prompt in prompts:
|
||||
prompt["images"] = publish_image_paths(
|
||||
prompt.get("images", []), self._public_path
|
||||
)
|
||||
return prompts
|
||||
|
||||
async def search_prompts(self, request):
|
||||
"""Search for prompts using multiple filter criteria."""
|
||||
try:
|
||||
@@ -195,7 +223,7 @@ class PromptRoutesMixin:
|
||||
folder=folder,
|
||||
sort=sort,
|
||||
)
|
||||
self._enrich_prompt_images(results)
|
||||
self._present_prompts(results)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
@@ -213,7 +241,11 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Search error: {e}", exc_info=True)
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Search failed: {str(e)}", "results": []},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Search failed: {safe_error_message(e)}",
|
||||
"results": [],
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -232,7 +264,9 @@ class PromptRoutesMixin:
|
||||
return web.json_response({"success": True, "subfolders": subfolders})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Subfolders error: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def get_recent_prompts(self, request):
|
||||
"""Retrieve prompts with pagination and an optional sort (default newest
|
||||
@@ -252,7 +286,7 @@ class PromptRoutesMixin:
|
||||
results = await self._run_in_executor(
|
||||
self.db.get_recent_prompts, limit=limit, offset=offset, sort=sort
|
||||
)
|
||||
self._enrich_prompt_images(results["prompts"])
|
||||
self._present_prompts(results["prompts"])
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
@@ -275,7 +309,7 @@ class PromptRoutesMixin:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to get recent prompts: {str(e)}",
|
||||
"error": f"Failed to get recent prompts: {safe_error_message(e)}",
|
||||
"results": [],
|
||||
"pagination": {"total": 0, "page": 1, "total_pages": 0},
|
||||
},
|
||||
@@ -292,7 +326,7 @@ class PromptRoutesMixin:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to get categories: {str(e)}",
|
||||
"error": f"Failed to get categories: {safe_error_message(e)}",
|
||||
"categories": [],
|
||||
},
|
||||
status=500,
|
||||
@@ -308,7 +342,7 @@ class PromptRoutesMixin:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to get tags: {str(e)}",
|
||||
"error": f"Failed to get tags: {safe_error_message(e)}",
|
||||
"tags": [],
|
||||
},
|
||||
status=500,
|
||||
@@ -346,7 +380,9 @@ class PromptRoutesMixin:
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Tags stats error: {e}", exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def get_tag_prompts(self, request):
|
||||
"""Get prompts for a single tag."""
|
||||
@@ -367,7 +403,7 @@ class PromptRoutesMixin:
|
||||
result = await self._run_in_executor(
|
||||
self.db.get_prompts_by_tags, [tag_name], "and", limit, offset
|
||||
)
|
||||
self._enrich_prompt_images(result["prompts"])
|
||||
self._present_prompts(result["prompts"])
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
@@ -384,7 +420,9 @@ class PromptRoutesMixin:
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Tag prompts error: {e}", exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def get_tags_filter(self, request):
|
||||
"""Get prompts matching multiple tags with AND/OR mode, or untagged prompts."""
|
||||
@@ -399,7 +437,7 @@ class PromptRoutesMixin:
|
||||
result = await self._run_in_executor(
|
||||
self.db.get_untagged_prompts, limit, offset
|
||||
)
|
||||
self._enrich_prompt_images(result["prompts"])
|
||||
self._present_prompts(result["prompts"])
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
@@ -434,7 +472,7 @@ class PromptRoutesMixin:
|
||||
result = await self._run_in_executor(
|
||||
self.db.get_prompts_by_tags, tags_list, mode, limit, offset
|
||||
)
|
||||
self._enrich_prompt_images(result["prompts"])
|
||||
self._present_prompts(result["prompts"])
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
@@ -452,7 +490,9 @@ class PromptRoutesMixin:
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Tags filter error: {e}", exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def rename_tag_endpoint(self, request):
|
||||
"""Rename a tag across all prompts."""
|
||||
@@ -495,7 +535,9 @@ class PromptRoutesMixin:
|
||||
return web.json_response(resp)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Rename tag error: {e}", exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def delete_tag_endpoint(self, request):
|
||||
"""Delete a tag from all prompts."""
|
||||
@@ -525,7 +567,9 @@ class PromptRoutesMixin:
|
||||
return web.json_response(resp)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Delete tag error: {e}", exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def merge_tags_endpoint(self, request):
|
||||
"""Merge source tags into a target tag."""
|
||||
@@ -566,7 +610,9 @@ class PromptRoutesMixin:
|
||||
return web.json_response(resp)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Merge tags error: {e}", exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
return web.json_response(
|
||||
{"success": False, "error": safe_error_message(e)}, status=500
|
||||
)
|
||||
|
||||
async def save_prompt(self, request):
|
||||
"""Save a new prompt with metadata and duplicate detection."""
|
||||
@@ -641,7 +687,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Save error: {e}", exc_info=True)
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to save prompt: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to save prompt: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -671,7 +720,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Delete error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to delete prompt: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to delete prompt: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -715,7 +767,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Update prompt error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to update prompt: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to update prompt: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -752,7 +807,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Update rating error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to update rating: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to update rating: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -795,7 +853,11 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Add tag error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to add tag: {str(e)}"}, status=500
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to add tag: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
async def add_tags_to_prompt(self, request):
|
||||
@@ -853,7 +915,11 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Add tags error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to add tags: {str(e)}"}, status=500
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to add tags: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
async def remove_prompt_tag(self, request):
|
||||
@@ -890,7 +956,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Remove tag error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to remove tag: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to remove tag: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -920,7 +989,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Bulk delete error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to delete prompts: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to delete prompts: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -952,7 +1024,11 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Bulk add tags error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to add tags: {str(e)}"}, status=500
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to add tags: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
async def bulk_set_category(self, request):
|
||||
@@ -982,7 +1058,10 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Bulk set category error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to set category: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to set category: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@@ -1013,6 +1092,9 @@ class PromptRoutesMixin:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Export error: {e}")
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Failed to export prompts: {str(e)}"},
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Failed to export prompts: {safe_error_message(e)}",
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user