You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
159 lines
5.2 KiB
159 lines
5.2 KiB
#!/usr/bin/env python3
|
|
"""启动图片选择预览服务器。
|
|
|
|
用法:python3 render_image_select.py --title "人设图选择" --images a.png b.png c.png d.png
|
|
|
|
展示候选图片供用户点击选择,选择结果写入 feedback.md,服务器自动退出。
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import mimetypes
|
|
import os
|
|
import sys
|
|
import threading
|
|
import webbrowser
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from pathlib import Path
|
|
|
|
SKILL_DIR = Path(__file__).resolve().parent.parent
|
|
TEMPLATE_DIR = SKILL_DIR / "html"
|
|
PROJECT_ROOT = Path.cwd() # MCP tool spawns with cwd = user project dir
|
|
WORK_DIR = PROJECT_ROOT / ".sword-dance"
|
|
|
|
|
|
def to_url(abs_path: str) -> str:
|
|
encoded = base64.urlsafe_b64encode(abs_path.encode()).decode()
|
|
try:
|
|
mtime = int(os.path.getmtime(abs_path))
|
|
except OSError:
|
|
mtime = 0
|
|
return f"/images/{encoded}?t={mtime}"
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
html_content: str = ""
|
|
feedback_path: Path = Path("feedback.md")
|
|
server_ref: HTTPServer | None = None
|
|
|
|
def do_GET(self):
|
|
if self.path.startswith("/images/"):
|
|
self._serve_image()
|
|
return
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
self.end_headers()
|
|
self.wfile.write(self.html_content.encode("utf-8"))
|
|
|
|
def _serve_image(self):
|
|
raw = self.path[len("/images/"):]
|
|
encoded = raw.split("?")[0] # strip cache-buster query
|
|
try:
|
|
img_path = Path(base64.urlsafe_b64decode(encoded).decode("utf-8"))
|
|
except Exception:
|
|
self.send_error(400)
|
|
return
|
|
if not img_path.exists():
|
|
self.send_error(404)
|
|
return
|
|
mime = mimetypes.guess_type(str(img_path))[0] or "image/png"
|
|
data = img_path.read_bytes()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", mime)
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
|
|
def do_POST(self):
|
|
if self.path == "/feedback":
|
|
length = int(self.headers.get("Content-Length", 0))
|
|
body = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
feedback = body.get("feedback", "").strip()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
if feedback:
|
|
self.feedback_path.write_text(feedback, encoding="utf-8")
|
|
self.wfile.write(json.dumps({"ok": True}).encode())
|
|
print(f"FEEDBACK_FILE={self.feedback_path}")
|
|
threading.Timer(0.5, self._shutdown).start()
|
|
else:
|
|
self.wfile.write(json.dumps({"ok": False}).encode())
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def _shutdown(self):
|
|
if self.server_ref:
|
|
self.server_ref.shutdown()
|
|
|
|
def log_message(self, fmt, *args):
|
|
pass
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--title", required=True, help="页面标题")
|
|
parser.add_argument("--images", nargs="+", required=True, help="候选图片路径列表")
|
|
parser.add_argument("--feedback", default=str(WORK_DIR / "feedback.md"),
|
|
help="反馈文件路径")
|
|
parser.add_argument("--lang", default="", help="UI language: zh/en")
|
|
parser.add_argument("--port", type=int, default=0)
|
|
args = parser.parse_args()
|
|
|
|
template_path = TEMPLATE_DIR / "preview-image_select.html"
|
|
if not template_path.exists():
|
|
print(f"ERROR: 模板不存在: {template_path}")
|
|
sys.exit(1)
|
|
|
|
feedback_path = Path(args.feedback)
|
|
feedback_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Build candidate data
|
|
candidates = []
|
|
for i, img_path in enumerate(args.images):
|
|
p = Path(img_path).resolve()
|
|
if not p.exists():
|
|
print(f"WARNING: 图片不存在: {p}")
|
|
continue
|
|
candidates.append({
|
|
"url": to_url(str(p)),
|
|
"label": f"Candidate {i + 1}" if args.lang == "en" else f"候选 {i + 1}",
|
|
"filename": p.name,
|
|
})
|
|
|
|
if not candidates:
|
|
print("ERROR: 没有有效的候选图片")
|
|
sys.exit(1)
|
|
|
|
data = {"title": args.title, "candidates": candidates}
|
|
|
|
template = template_path.read_text(encoding="utf-8")
|
|
injected = template.replace(
|
|
"const PREVIEW_DATA = null; /* __PREVIEW_DATA__ */",
|
|
f"const PREVIEW_DATA = {json.dumps(json.dumps(data, ensure_ascii=False))};",
|
|
)
|
|
injected = injected.replace(
|
|
"const PREVIEW_LANG = null; /* __PREVIEW_LANG__ */",
|
|
f"const PREVIEW_LANG = {json.dumps(args.lang or None)};",
|
|
)
|
|
|
|
server = HTTPServer(("127.0.0.1", args.port), Handler)
|
|
port = server.server_address[1]
|
|
|
|
Handler.html_content = injected
|
|
Handler.feedback_path = feedback_path
|
|
Handler.server_ref = server
|
|
|
|
url = f"http://127.0.0.1:{port}"
|
|
print(f"PREVIEW_URL={url}")
|
|
print("等待用户选择...")
|
|
|
|
webbrowser.open(url)
|
|
server.serve_forever()
|
|
print("服务器已停止")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|