Popiai-skill仓库
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

#!/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()