Merge remote-tracking branch 'origin/master'
# Conflicts: # docker-compose.yml
This commit is contained in:
@@ -16,11 +16,14 @@ from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request as UrlRequest, urlopen
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core import config as wc_config
|
||||
from core.fonts import get_cached_font
|
||||
@@ -82,6 +85,12 @@ METADATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ORDERS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PRODUCT_ARCHIVES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# The converter intentionally runs as a separate, unprivileged container. Do
|
||||
# not expose it publicly: the application validates payload size and forwards
|
||||
# only generated SVG documents to this private service.
|
||||
AI_CONVERTER_URL = os.environ.get("AI_CONVERTER_URL", "").rstrip("/")
|
||||
AI_EXPORT_MAX_BYTES = 25 * 1024 * 1024
|
||||
|
||||
metadata_store = MetadataStore(METADATA_DIR / "app.db")
|
||||
manager = JobManager(metadata_store)
|
||||
storage = Storage(WORKSPACE_DIR)
|
||||
@@ -1019,6 +1028,65 @@ def get_file(job_id: str, kind: str):
|
||||
return FileResponse(path, media_type=media, filename=path.name)
|
||||
|
||||
|
||||
class AiExportRequest(BaseModel):
|
||||
"""The SVG is produced by this application, then converted in a private service."""
|
||||
|
||||
svg: str
|
||||
filename: str = "wordcloud.ai"
|
||||
|
||||
|
||||
def _ai_download_name(value: str) -> str:
|
||||
stem = Path(value).stem
|
||||
stem = re.sub(r"[^A-Za-z0-9._-]+", "-", stem).strip(".-")[:80]
|
||||
return f"{stem or 'wordcloud'}.ai"
|
||||
|
||||
|
||||
@app.post("/api/exports/ai")
|
||||
def export_ai(payload: AiExportRequest):
|
||||
"""Convert an application-generated SVG to an Illustrator-compatible AI file.
|
||||
|
||||
The conversion service has no public port. Keeping this proxy in the main
|
||||
API lets the frontend retain its same-origin API and gives us one place to
|
||||
enforce file-size limits and translate converter errors.
|
||||
"""
|
||||
if not AI_CONVERTER_URL:
|
||||
raise HTTPException(status_code=503, detail="AI 转换服务尚未配置")
|
||||
|
||||
svg_bytes = payload.svg.encode("utf-8")
|
||||
if not svg_bytes:
|
||||
raise HTTPException(status_code=400, detail="SVG 内容不能为空")
|
||||
if len(svg_bytes) > AI_EXPORT_MAX_BYTES:
|
||||
raise HTTPException(status_code=413, detail="SVG 文件超过 25 MB 限制")
|
||||
|
||||
request = UrlRequest(
|
||||
f"{AI_CONVERTER_URL}/convert",
|
||||
data=svg_bytes,
|
||||
headers={"Content-Type": "image/svg+xml; charset=utf-8"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=60) as response:
|
||||
ai_bytes = response.read()
|
||||
except HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")[:1000]
|
||||
try:
|
||||
detail = json.loads(detail).get("detail", detail)
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
raise HTTPException(status_code=422 if exc.code == 422 else 502, detail=f"AI 转换失败: {detail}") from exc
|
||||
except URLError as exc:
|
||||
raise HTTPException(status_code=503, detail="AI 转换服务不可用") from exc
|
||||
|
||||
if not ai_bytes:
|
||||
raise HTTPException(status_code=502, detail="AI 转换服务未返回文件")
|
||||
filename = _ai_download_name(payload.filename)
|
||||
return StreamingResponse(
|
||||
io.BytesIO(ai_bytes),
|
||||
media_type="application/postscript",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 3.9 生产订单列表(下单派单投递的 WCD 生产任务,需登录)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user