Files

408 lines
16 KiB
Python

import asyncio
import json
import os
import time
import urllib.request
import urllib.parse
import folder_paths
from aiohttp import web
import server
import re
from . import utils
prompt_server = server.PromptServer.instance
main_loop = asyncio.get_event_loop()
def sanitize_filename(filename):
filename = filename.replace("..", "").replace("\0", "")
illegal_chars = r'<>:"/\\|?*\t\n\r'
sanitized = re.sub(f"[{re.escape(illegal_chars)}]", "_", filename)
name, ext = os.path.splitext(sanitized)
name = name[:150]
return f"{name}{ext}"
@prompt_server.routes.get("/civitai_utils/get_db_stats")
async def get_db_stats(request):
try:
stats = utils.db_manager.get_db_stats()
return web.json_response({"status": "ok", "stats": stats})
except Exception as e:
print(f"[Civitai Utils] Error getting DB stats: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.get("/civitai_utils/get_scanned_models")
async def get_scanned_models(request):
"""获取数据库中已扫描模型列表的API"""
model_type = request.query.get("model_type")
if not model_type or model_type not in ["checkpoints", "loras"]:
return web.json_response(
{"status": "error", "message": "Invalid model_type"}, status=400
)
try:
# force_sync=False 避免不必要的重复扫描
model_list = utils.get_model_filenames_from_db(model_type, force_sync=False)
return web.json_response({"status": "ok", "models": model_list})
except Exception as e:
print(f"[Civitai Utils] Error getting scanned models: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.get("/civitai_utils/check_legacy_cache")
async def check_legacy_cache(request):
"""检查旧版缓存文件是否存在的API"""
try:
exists = utils.check_legacy_cache_exists()
return web.json_response({"exists": exists})
except Exception as e:
return web.json_response({"exists": False, "error": str(e)})
@prompt_server.routes.post("/civitai_utils/force_rescan")
async def force_rescan(request):
try:
data = await request.json()
model_type = data.get("model_type")
rehash_all = data.get("rehash_all", False)
if not model_type or model_type not in ["checkpoints", "loras"]:
return web.json_response(
{"status": "error", "message": "Invalid model_type"}, status=400
)
# 如果是 rehash_all,我们需要先清空数据库中的 mtime
if rehash_all:
with utils.db_manager.get_connection() as conn:
conn.execute(
"UPDATE versions SET local_mtime = 0 WHERE model_type = ?",
(model_type,),
)
# 将计时器清零以确保扫描执行
utils.db_manager.set_setting(f"last_sync_{model_type}", 0)
# 执行扫描并捕获结果
scan_results = utils.sync_local_files_with_db(model_type, force=True)
found_count = scan_results.get("found", 0)
hashed_count = scan_results.get("hashed", 0)
if rehash_all:
message = (
f"Re-hash complete! {hashed_count} {model_type} files were re-hashed."
)
elif found_count > 0:
message = f"Rescan complete! Found and processed {hashed_count} new/modified {model_type} files."
else:
message = f"Rescan complete. No new or modified {model_type} files found."
return web.json_response({"status": "ok", "message": message})
except Exception as e:
print(f"[Civitai Utils] Error forcing rescan: {e}")
import traceback
traceback.print_exc()
return web.json_response({"status": "error", "message": str(e)}, status=500)
# 从旧版JSON迁移哈希的API
@prompt_server.routes.post("/civitai_utils/migrate_hashes")
async def migrate_hashes(request):
try:
results = utils.migrate_legacy_caches()
return web.json_response({"status": "ok", "message": results["message"]})
except Exception as e:
print(f"[Civitai Utils] Error migrating hashes: {e}")
import traceback
traceback.print_exc()
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.post("/civitai_utils/clear_cache")
async def clear_cache(request):
try:
data = await request.json()
cache_type = data.get("cache_type")
if cache_type == "analysis":
utils.db_manager.clear_analysis_cache()
message = "Analyzer cache cleared successfully."
elif cache_type == "api_responses":
utils.db_manager.clear_api_responses()
message = "API response cache cleared successfully."
elif cache_type == "triggers":
utils.db_manager.clear_all_triggers()
message = "Trigger word cache cleared successfully."
elif cache_type == "all":
utils.db_manager.clear_analysis_cache()
utils.db_manager.clear_api_responses()
utils.db_manager.clear_all_triggers()
message = "All caches have been cleared."
else:
return web.json_response(
{"status": "error", "message": "Invalid cache type"}, status=400
)
return web.json_response({"status": "ok", "message": message})
except Exception as e:
print(f"[Civitai Utils] Error clearing cache: {e}")
import traceback
traceback.print_exc()
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.get("/civitai_recipe_finder/fetch_data")
async def fetch_data(request):
try:
model_type, model_filename, sort, nsfw_level, limit, filter_type = (
request.query.get("model_type"),
request.query.get("model_filename"),
request.query.get("sort"),
request.query.get("nsfw_level"),
int(request.query.get("limit", 32)),
request.query.get("filter_type"),
)
_, filename_to_hash = utils.get_local_model_maps(model_type)
model_hash = filename_to_hash.get(model_filename)
if not model_hash:
raise FileNotFoundError(f"Model hash not found for: {model_filename} in type {model_type}")
gallery_data = utils.fetch_civitai_data_by_hash(
model_hash,
sort,
limit,
nsfw_level,
filter_type if filter_type != "all" else None,
)
return web.json_response({"status": "ok", "images": gallery_data})
except Exception as e:
print(f"Error in fetch_data: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.post("/civitai_recipe_finder/set_selection")
async def set_selection(request):
try:
data = await request.json()
node_id, item, download_image = (
str(data.get("node_id")),
data.get("item"),
data.get("download_image", False),
)
selections = utils.load_selections()
selections[node_id] = {"item": item, "download_image": download_image}
utils.save_selections(selections)
utils.db_manager.set_setting("last_selection_time", time.time())
return web.json_response({"status": "ok"})
except Exception as e:
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.post("/civitai_recipe_finder/save_original_image")
async def save_original_image(request):
try:
data = await request.json()
image_url = data.get("url")
if not image_url:
return web.json_response(
{"status": "error", "message": "URL is missing"}, status=400
)
clean_url = re.sub(r"/(width|height|fit|quality|format)=\w+", "", image_url)
image_record = utils.db_manager.get_image_by_url(clean_url)
if image_record and image_record["local_filename"]:
local_path = os.path.join(
folder_paths.get_output_directory(), image_record["local_filename"]
)
if os.path.exists(local_path):
return web.json_response(
{
"status": "exists",
"message": f"Image already exists: {image_record['local_filename']}",
}
)
headers = {"User-Agent": "Mozilla/5.0"}
req = urllib.request.Request(clean_url, headers=headers)
base_filename = os.path.basename(urllib.parse.urlparse(clean_url).path)
sanitized_base = sanitize_filename(base_filename)
filename = f"civitai_{int(time.time())}_{sanitized_base}"
output_path = os.path.join(folder_paths.get_output_directory(), filename)
with (
urllib.request.urlopen(req, timeout=20) as response,
open(output_path, "wb") as f,
):
f.write(response.read())
utils.db_manager.add_downloaded_image(url=clean_url, local_filename=filename)
return web.json_response(
{"status": "ok", "message": f"Image saved as {filename}"}
)
except Exception as e:
print(f"[Civitai Utils] Error in save_original_image: {e}")
import traceback
traceback.print_exc()
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.post("/civitai_recipe_finder/get_workflow_source")
async def get_workflow_source(request):
try:
data = await request.json()
image_url = data.get("url")
if not image_url:
return web.Response(status=400, text="URL is missing")
clean_url = re.sub(r"/(width|height|fit|quality|format)=\w+", "", image_url)
image_record = utils.db_manager.get_image_by_url(clean_url)
if image_record and image_record["local_filename"]:
local_path = os.path.join(
folder_paths.get_output_directory(), image_record["local_filename"]
)
if os.path.exists(local_path):
with open(local_path, "rb") as f:
image_data = f.read()
ext = os.path.splitext(image_record["local_filename"])[1].lower()
content_type = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
}.get(ext, "image/png")
return web.Response(body=image_data, content_type=content_type)
headers = {"User-Agent": "Mozilla/5.0"}
req = urllib.request.Request(clean_url, headers=headers)
with urllib.request.urlopen(req, timeout=20) as response:
image_data = response.read()
content_type = response.headers.get("Content-Type", "image/png")
filename_base = os.path.basename(urllib.parse.urlparse(clean_url).path)
sanitized_base = sanitize_filename(filename_base)
filename = f"civitai_{int(time.time())}_{sanitized_base}"
if not any(
filename.lower().endswith(ext) for ext in [".png", ".jpg", ".jpeg", ".webp"]
):
ext = "." + content_type.split("/")[-1] if "/" in content_type else ".png"
filename += ext
output_path = os.path.join(folder_paths.get_output_directory(), filename)
with open(output_path, "wb") as f:
f.write(image_data)
utils.db_manager.add_downloaded_image(url=clean_url, local_filename=filename)
return web.Response(body=image_data, content_type=content_type)
except Exception as e:
return web.Response(status=500, text=str(e))
@prompt_server.routes.get("/civitai_utils/get_config")
async def get_config(request):
api_key = utils.db_manager.get_setting("civitai_api_key")
config = {
"network_choice": utils.db_manager.get_setting("network_choice", "com"),
"api_key_exists": bool(api_key)
}
return web.json_response(config)
@prompt_server.routes.post("/civitai_utils/set_config")
async def set_config(request):
try:
data = await request.json()
if "network_choice" in data:
utils.db_manager.set_setting("network_choice", data["network_choice"])
if "api_key" in data:
utils.db_manager.set_setting("civitai_api_key", data["api_key"])
return web.json_response({"status": "ok"})
except Exception as e:
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.get("/civitai_utils/get_local_models")
async def get_local_models(request):
"""
一个强大、统一的API,一次性获取所有处理好的模型数据。
"""
try:
loop = asyncio.get_event_loop()
force_refresh = request.query.get("force_refresh", "false").lower() == "true"
# 将所有耗时的操作放入线程池,避免阻塞主服务器线程
models = await loop.run_in_executor(
None, utils.get_all_local_models_with_details, force_refresh
)
return web.json_response({"status": "ok", "models": models})
except Exception as e:
print(f"[Civitai Utils] FATAL ERROR in get_local_models API: {e}")
import traceback
traceback.print_exc()
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.get("/civitai_recipe_finder/get_local_hashes")
async def get_local_hashes(request):
"""
一个轻量级的API,仅返回数据库中所有本地文件的哈希列表。
"""
try:
with utils.db_manager.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT hash FROM versions WHERE hash IS NOT NULL AND local_path IS NOT NULL"
)
# 使用集合推导式以获得最佳性能
hashes = {row["hash"] for row in cursor.fetchall()}
return web.json_response({"status": "ok", "hashes": list(hashes)})
except Exception as e:
print(f"[Civitai Utils] Error getting local hashes: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@prompt_server.routes.get("/civitai_utils/get_scan_status")
async def get_scan_status(request):
"""用于查询后台扫描状态"""
is_complete = utils.db_manager.get_setting("initial_scan_complete", False)
return web.json_response({"status": "ok", "is_scanning": not is_complete})
# --- WebSocket 部分 ---
ACTIVE_CONNECTIONS = set()
@prompt_server.routes.get("/civitai_toolkit/ws")
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
ACTIVE_CONNECTIONS.add(ws)
print("[Civitai Toolkit] New WebSocket connection established.")
try:
async for msg in ws:
pass
except Exception as e:
print(f"[Civitai Toolkit] WebSocket connection error: {e}")
finally:
ACTIVE_CONNECTIONS.remove(ws)
print("[Civitai Toolkit] WebSocket connection closed.")
return ws
async def send_ws_message_async(msg_type, data):
""" 异步发送函数 """
message = json.dumps({"type": msg_type, "data": data})
tasks = [conn.send_str(message) for conn in ACTIVE_CONNECTIONS]
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
def send_ws_message(msg_type, data):
"""
[核心修改] 这是供后台线程调用的线程安全函数
"""
if not main_loop or not main_loop.is_running():
print("[Civitai Toolkit] Main event loop not available for WebSocket message.")
return
# 使用 run_coroutine_threadsafe 从同步线程向异步循环提交任务
asyncio.run_coroutine_threadsafe(
send_ws_message_async(msg_type, data),
main_loop
)