diff --git a/lib/image-delivery.ts b/lib/image-delivery.ts index db3bc49bc..1deac343a 100644 --- a/lib/image-delivery.ts +++ b/lib/image-delivery.ts @@ -53,12 +53,39 @@ function isTrustedDirectStorageImageHost(hostname: string): boolean { ); } -function resolveTrustedDirectStorageDisplayUrl(url: string): string | null { +function isFirstPartyQiniuAsset(target: URL) { + return ( + target.hostname.trim().toLowerCase() === "bbyy.ai" && + target.pathname.startsWith("/uploads/") + ); +} + +function resolveTrustedDirectStorageDisplayUrl( + url: string, + options: Pick +): string | null { try { const target = new URL(url); - return isTrustedDirectStorageImageHost(target.hostname) - ? target.toString() - : null; + if ( + !isTrustedDirectStorageImageHost(target.hostname) && + !isFirstPartyQiniuAsset(target) + ) { + return null; + } + + const width = normalizeWidth(options.width); + const quality = normalizeQuality(options.quality); + const format = options.format; + if ((!width && !quality && !format) || target.search) { + return target.toString(); + } + + const operations = ["imageView2", "2"]; + if (width) operations.push("w", String(width)); + if (format) operations.push("format", format === "jpeg" ? "jpg" : format); + if (quality) operations.push("q", String(quality)); + target.search = `?${operations.join("/")}`; + return target.toString(); } catch { return null; } @@ -116,7 +143,10 @@ export function buildImageProxyUrl(url: string, options: ImageProxyOptions = {}) return source; } - const trustedDirectStorageUrl = resolveTrustedDirectStorageDisplayUrl(source); + const trustedDirectStorageUrl = resolveTrustedDirectStorageDisplayUrl( + source, + options + ); if (trustedDirectStorageUrl) { return trustedDirectStorageUrl; } diff --git a/playwright.helpers.config.ts b/playwright.helpers.config.ts index 382ff7f7b..f2cf47ad9 100644 --- a/playwright.helpers.config.ts +++ b/playwright.helpers.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ /routes\/canvas-agent-operations\.test\.ts/, /routes\/canvas-overlay-density\.test\.ts/, /routes\/image-message-contracts\.test\.ts/, + /routes\/image-delivery-qiniu\.test\.ts/, /routes\/production-local-fallbacks\.test\.ts/, /routes\/canvas-entry-performance\.test\.ts/, /routes\/settings-visual-consistency\.test\.ts/, diff --git a/scripts/deploy-production-atomic.sh b/scripts/deploy-production-atomic.sh index 5b4dd4c17..ca971a3e3 100755 --- a/scripts/deploy-production-atomic.sh +++ b/scripts/deploy-production-atomic.sh @@ -28,6 +28,7 @@ docker run --rm \ test -s "${candidate_dir}/BUILD_ID" docker stop ai-chatbot-app >/dev/null +docker rm ai-chatbot-app >/dev/null if [ -d "${app_dir}/.next" ]; then mv "${app_dir}/.next" "$previous_dir" fi @@ -39,6 +40,7 @@ docker compose -f "${compose_dir}/docker-compose.yml" up -d app if ! curl -fsS --retry 12 --retry-delay 2 --max-time 10 \ http://127.0.0.1:3125/login >/dev/null; then docker stop ai-chatbot-app >/dev/null 2>&1 || true + docker rm ai-chatbot-app >/dev/null 2>&1 || true failed_dir="${app_dir}/.next-failed-$(date +%Y%m%dT%H%M%S)" mv "${app_dir}/.next" "$failed_dir" if [ -d "$previous_dir" ]; then diff --git a/tests/routes/image-delivery-qiniu.test.ts b/tests/routes/image-delivery-qiniu.test.ts new file mode 100644 index 000000000..e4c8891fd --- /dev/null +++ b/tests/routes/image-delivery-qiniu.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "@playwright/test"; +import { buildImageProxyUrl } from "@/lib/image-delivery"; + +test.describe("Qiniu image delivery", () => { + test("uses Qiniu image processing for direct canvas previews", () => { + expect( + buildImageProxyUrl("https://yybb.top/uploads/result.png", { + width: 1200, + quality: 80, + format: "webp", + }) + ).toBe( + "https://yybb.top/uploads/result.png?imageView2/2/w/1200/format/webp/q/80" + ); + }); + + test("optimizes legacy first-party upload URLs without changing their identity", () => { + expect( + buildImageProxyUrl("https://bbyy.ai/uploads/result.png", { + width: 960, + quality: 88, + format: "webp", + }) + ).toBe( + "https://bbyy.ai/uploads/result.png?imageView2/2/w/960/format/webp/q/88" + ); + }); + + test("does not overwrite an existing storage query", () => { + const url = "https://yybb.top/uploads/result.png?imageView2/2/w/400"; + expect(buildImageProxyUrl(url, { width: 1200, format: "webp" })).toBe(url); + }); + + test("keeps unrelated first-party URLs out of the Qiniu path", () => { + const result = buildImageProxyUrl("https://bbyy.ai/logo.png", { + delivery: "proxy", + width: 320, + }); + expect(result).toContain("/api/image-proxy?"); + }); +});