Files

1 line
184 KiB
JSON

{"repo": "beefinwater/logo-master", "files": {"server.js": {"850b24e641c564062f901bd79a826bac72522d62": {"t": 1785745540.8637445, "n": 86906, "h": "f13115539aeadc7d", "c": "const http = require(\"node:http\");\nconst fs = require(\"node:fs\");\nconst path = require(\"node:path\");\nconst crypto = require(\"node:crypto\");\nconst os = require(\"node:os\");\nconst { execFile } = require(\"node:child_process\");\nconst sharp = require(\"sharp\");\nconst AdmZip = require(\"adm-zip\");\n\nconst rootDir = __dirname;\nconst appConfig = loadAppConfig();\nconst outputDir = path.join(rootDir, \"generated\");\nconst exportDir = path.join(rootDir, \"exports\");\nconst uploadDir = path.join(rootDir, \"uploads\");\nconst port = Number(process.env.PORT || 8787);\n\nloadLocalEnv();\n\nconst apiKey = process.env.OPENAI_API_KEY || \"\";\nconst aiProvider = (process.env.AI_PROVIDER || appConfig.models?.text?.provider || \"openai\").toLowerCase();\nconst imageProvider = (process.env.IMAGE_PROVIDER || appConfig.models?.image?.provider || \"openai\").toLowerCase();\nconst kimiApiKey = process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || \"\";\nconst kimiBaseUrl = (process.env.KIMI_BASE_URL || \"https://api.moonshot.cn/v1\").replace(/\\/+$/, \"\");\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || \"\";\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || appConfig.models?.text?.base_url || \"https://api.deepseek.com/v1\").replace(/\\/+$/, \"\");\nconst dashscopeApiKey = process.env.DASHSCOPE_API_KEY || process.env.ALIYUN_API_KEY || \"\";\nconst dashscopeBaseUrl = (process.env.DASHSCOPE_BASE_URL || \"https://dashscope.aliyuncs.com/api/v1\").replace(/\\/+$/, \"\");\nconst arkApiKey = process.env.ARK_API_KEY || process.env.VOLCENGINE_API_KEY || \"\";\nconst arkBaseUrl = (process.env.ARK_BASE_URL || \"https://ark.cn-beijing.volces.com/api/v3\").replace(/\\/+$/, \"\");\nconst textModel = process.env.OPENAI_TEXT_MODEL || \"gpt-4.1\";\nconst kimiTextModel = process.env.KIMI_TEXT_MODEL || \"kimi-k2.6\";\nconst deepseekTextModel = process.env.DEEPSEEK_TEXT_MODEL || appConfig.models?.text?.model || \"deepseek-v4-pro\";\nconst aliyunTextModel = process.env.ALIYUN_TEXT_MODEL || \"qwen-plus\";\nconst volcengineTextModel = process.env.VOLCENGINE_TEXT_MODEL || process.env.ARK_TEXT_MODEL || \"doubao-seed-1-6-251015\";\nconst imageModel = process.env.OPENAI_IMAGE_MODEL || \"gpt-image-1\";\nconst kimiImageModel = process.env.KIMI_IMAGE_MODEL || process.env.KIMI_TEXT_MODEL || \"kimi-k2.6\";\nconst kimiImageEndpoint = process.env.KIMI_IMAGE_ENDPOINT || `${kimiBaseUrl}/images/generations`;\nconst aliyunImageModel = process.env.ALIYUN_IMAGE_MODEL || \"wan2.7-image\";\nconst aliyunImageSize = process.env.ALIYUN_IMAGE_SIZE || \"1K\";\nconst volcengineImageModel = process.env.VOLCENGINE_IMAGE_MODEL || process.env.ARK_IMAGE_MODEL || appConfig.models?.image?.model || \"doubao-seedream-3-0-t2i-250415\";\nconst volcengineImageSize = process.env.VOLCENGINE_IMAGE_SIZE || appConfig.models?.image?.size || \"2048x2048\";\n\nfs.mkdirSync(outputDir, { recursive: true });\nfs.mkdirSync(exportDir, { recursive: true });\nfs.mkdirSync(uploadDir, { recursive: true });\n\nfunction loadLocalEnv() {\n const envPath = path.join(rootDir, \".env.local\");\n if (!fs.existsSync(envPath)) return;\n\n const lines = fs.readFileSync(envPath, \"utf8\").split(/\\r?\\n/);\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) continue;\n const key = trimmed.slice(0, eq).trim();\n const value = trimmed.slice(eq + 1).trim().replace(/^[\"']|[\"']$/g, \"\");\n if (key && process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n}\n\nfunction loadAppConfig() {\n const configPath = path.join(rootDir, \"icon-agent.config.json\");\n if (!fs.existsSync(configPath)) return {};\n try {\n return JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n } catch (error) {\n console.warn(`Failed to read icon-agent.config.json: ${error.message}`);\n return {};\n }\n}\n\nconst mimeTypes = {\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"application/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".webp\": \"image/webp\",\n \".svg\": \"image/svg+xml\",\n \".zip\": \"application/zip\",\n};\n\nfunction sendJson(res, status, payload) {\n res.writeHead(status, {\n ...corsHeaders(),\n \"Content-Type\": \"application/json; charset=utf-8\",\n });\n res.end(JSON.stringify(payload));\n}\n\nfunction corsHeaders() {\n return {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Methods\": \"GET,POST,OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type,Authorization\",\n };\n}\n\nfunction readBody(req) {\n return new Promise((resolve, reject) => {\n let body = \"\";\n req.on(\"data\", (chunk) => {\n body += chunk;\n if (body.length > 30 * 1024 * 1024) {\n reject(new Error(\"Request body is too large.\"));\n req.destroy();\n }\n });\n req.on(\"end\", () => {\n try {\n resolve(body ? JSON.parse(body) : {});\n } catch (error) {\n reject(error);\n }\n });\n req.on(\"error\", reject);\n });\n}\n\nfunction parseDataUrl(dataUrl = \"\") {\n const match = String(dataUrl).match(/^data:([^;,]+);base64,(.+)$/s);\n if (!match) throw new Error(\"Invalid image data URL.\");\n return {\n mime: match[1].toLowerCase(),\n buffer: Buffer.from(match[2], \"base64\"),\n };\n}\n\nfunction renderTemplate(template = \"\", values = {}) {\n return String(template).replace(/\\{([A-Z0-9_]+)\\}/g, (_, key) => {\n const value = values[key];\n if (typeof value === \"string\") return value;\n return JSON.stringify(value ?? \"\", null, 2);\n });\n}\n\nfunction extensionFromMime(mime = \"\") {\n if (mime === \"image/jpeg\" || mime === \"image/jpg\") return \".jpg\";\n if (mime === \"image/png\") return \".png\";\n if (mime === \"image/webp\") return \".webp\";\n if (mime === \"image/gif\") return \".gif\";\n return \"\";\n}\n\nfunction publicBaseUrl(req) {\n const configured = (process.env.PUBLIC_BASE_URL || process.env.APP_PUBLIC_URL || \"\").replace(/\\/+$/, \"\");\n if (configured) return configured;\n const proto = req.headers[\"x-forwarded-proto\"] || (req.socket.encrypted ? \"https\" : \"http\");\n const host = req.headers[\"x-forwarded-host\"] || req.headers.host || `localhost:${port}`;\n return `${String(proto).split(\",\")[0]}://${String(host).split(\",\")[0]}`;\n}\n\nasync function uploadReferenceImage(input, req) {\n const { mime, buffer } = parseDataUrl(input.dataUrl || input.image || \"\");\n if (!mime.startsWith(\"image/\")) throw new Error(\"Only image uploads are supported.\");\n if (buffer.length > 8 * 1024 * 1024) throw new Error(\"Reference image must be 8MB or smaller.\");\n\n const ext = extensionFromMime(mime);\n if (!ext) throw new Error(`Unsupported image type: ${mime}`);\n\n const safeBase = sanitizeFileName(path.basename(input.name || \"reference\", path.extname(input.name || \"\"))) || \"reference\";\n const id = `${Date.now()}_${crypto.randomBytes(5).toString(\"hex\")}`;\n const filename = `${safeBase}_${id}${ext}`;\n const filePath = path.join(uploadDir, filename);\n fs.writeFileSync(filePath, buffer);\n\n const urlPath = `/uploads/${filename}`;\n return {\n id,\n name: input.name || filename,\n type: mime,\n size: buffer.length,\n url: urlPath,\n public_url: `${publicBaseUrl(req)}${urlPath}`,\n };\n}\n\nasync function openaiFetch(endpoint, payload) {\n if (!apiKey) {\n throw new Error(\"Missing OPENAI_API_KEY. Please set it before starting the server.\");\n }\n\n try {\n return await openaiFetchWithNode(endpoint, payload);\n } catch (error) {\n if (!/fetch failed|Connect Timeout|UND_ERR_CONNECT_TIMEOUT/i.test(error.message + \" \" + (error.cause?.code || \"\"))) {\n throw error;\n }\n console.warn(\"Node fetch failed; retrying OpenAI request through PowerShell HTTPS.\");\n return openaiFetchWithPowerShell(endpoint, payload);\n }\n}\n\nasync function kimiFetch(endpoint, payload) {\n if (!kimiApiKey) {\n throw new Error(\"Missing KIMI_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${kimiBaseUrl}/${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${kimiApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function deepseekFetch(endpoint, payload) {\n if (!deepseekApiKey) {\n throw new Error(\"Missing DEEPSEEK_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${deepseekBaseUrl}/${endpoint.replace(/^\\/+/, \"\")}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${deepseekApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function kimiFetchAbsolute(url, payload) {\n if (!kimiApiKey) {\n throw new Error(\"Missing KIMI_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${kimiApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function dashscopeFetch(pathname, payload, options = {}) {\n if (!dashscopeApiKey) {\n throw new Error(\"Missing DASHSCOPE_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${dashscopeBaseUrl}/${pathname.replace(/^\\/+/, \"\")}`, {\n method: options.method || \"POST\",\n headers: {\n Authorization: `Bearer ${dashscopeApiKey}`,\n \"Content-Type\": \"application/json\",\n ...(options.async ? { \"X-DashScope-Async\": \"enable\" } : {}),\n },\n body: payload ? JSON.stringify(payload) : undefined,\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function dashscopeGet(pathname) {\n if (!dashscopeApiKey) {\n throw new Error(\"Missing DASHSCOPE_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${dashscopeBaseUrl}/${pathname.replace(/^\\/+/, \"\")}`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${dashscopeApiKey}` },\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function arkFetch(pathname, payload) {\n if (!arkApiKey) {\n throw new Error(\"Missing ARK_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${arkBaseUrl}/${pathname.replace(/^\\/+/, \"\")}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${arkApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function openaiFetchWithNode(endpoint, payload) {\n const response = await fetch(`https://api.openai.com/v1/${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nfunction parseOpenAIResponse(status, statusText, text) {\n let data;\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = { raw: text };\n }\n\n if (status < 200 || status >= 300) {\n const message = data?.error?.message || data?.raw || statusText || `HTTP ${status}`;\n throw new Error(message);\n }\n\n return data;\n}\n\nfunction psQuote(value) {\n return `'${String(value).replace(/'/g, \"''\")}'`;\n}\n\nfunction execFileAsync(file, args, options = {}) {\n return new Promise((resolve, reject) => {\n execFile(file, args, { maxBuffer: 1024 * 1024 * 80, ...options }, (error, stdout, stderr) => {\n if (error) {\n error.stdout = stdout;\n error.stderr = stderr;\n reject(error);\n return;\n }\n resolve({ stdout, stderr });\n });\n });\n}\n\nasync function openaiFetchWithPowerShell(endpoint, payload) {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"icon-agent-openai-\"));\n const bodyPath = path.join(tmpDir, \"request.json\");\n const responsePath = path.join(tmpDir, \"response.json\");\n const statusPath = path.join(tmpDir, \"status.txt\");\n fs.writeFileSync(bodyPath, JSON.stringify(payload), \"utf8\");\n\n const command = `\n$ErrorActionPreference = \"Stop\"\n$body = Get-Content -LiteralPath ${psQuote(bodyPath)} -Raw\ntry {\n Add-Type -AssemblyName System.Net.Http\n $client = [System.Net.Http.HttpClient]::new()\n $client.Timeout = [TimeSpan]::FromSeconds(300)\n $client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new(\"Bearer\", $env:OPENAI_API_KEY)\n $content = [System.Net.Http.StringContent]::new($body, [System.Text.Encoding]::UTF8, \"application/json\")\n $response = $client.PostAsync(${psQuote(`https://api.openai.com/v1/${endpoint}`)}, $content).Result\n $responseBody = $response.Content.ReadAsStringAsync().Result\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value ([int]$response.StatusCode) -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(responsePath)} -Value $responseBody -Encoding UTF8\n if (-not $response.IsSuccessStatusCode) { exit 1 }\n} catch {\n $status = 500\n $content = $_.Exception.Message\n if (Test-Path ${psQuote(statusPath)}) { $status = [int](Get-Content -LiteralPath ${psQuote(statusPath)} -Raw) }\n if (Test-Path ${psQuote(responsePath)}) { $content = Get-Content -LiteralPath ${psQuote(responsePath)} -Raw }\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value $status -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(responsePath)} -Value $content -Encoding UTF8\n exit 1\n}\n`;\n\n try {\n await execFileAsync(\"powershell\", [\"-NoProfile\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", command], {\n env: { ...process.env, OPENAI_API_KEY: apiKey },\n });\n } catch {\n // The PowerShell script writes the response body/status even for OpenAI 4xx/5xx errors.\n }\n\n const status = Number(fs.existsSync(statusPath) ? fs.readFileSync(statusPath, \"utf8\").trim() : \"500\");\n const text = fs.existsSync(responsePath) ? fs.readFileSync(responsePath, \"utf8\") : \"\";\n fs.rmSync(tmpDir, { recursive: true, force: true });\n return parseOpenAIResponse(status, `HTTP ${status}`, text);\n}\n\nasync function fetchTextWithFallback(url) {\n try {\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n },\n });\n if (response.ok) return response.text();\n throw new Error(`HTTP ${response.status}`);\n } catch {\n return fetchTextWithPowerShell(url);\n }\n}\n\nasync function fetchTextWithPowerShell(url) {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"icon-agent-web-\"));\n const outputPath = path.join(tmpDir, \"response.txt\");\n const statusPath = path.join(tmpDir, \"status.txt\");\n const command = `\n$ErrorActionPreference = \"Stop\"\ntry {\n $headers = @{ \"User-Agent\" = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"; \"Accept-Language\" = \"en-US,en;q=0.9\" }\n $response = Invoke-WebRequest -Uri ${psQuote(url)} -Headers $headers -TimeoutSec 60\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value ([int]$response.StatusCode) -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(outputPath)} -Value $response.Content -Encoding UTF8\n} catch {\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value 500 -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(outputPath)} -Value $_.Exception.Message -Encoding UTF8\n exit 1\n}\n`;\n\n try {\n await execFileAsync(\"powershell\", [\"-NoProfile\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", command], {\n maxBuffer: 1024 * 1024 * 30,\n });\n } catch {\n // Read the captured response below.\n }\n\n const status = Number(fs.existsSync(statusPath) ? fs.readFileSync(statusPath, \"utf8\").trim() : \"500\");\n const text = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, \"utf8\") : \"\";\n fs.rmSync(tmpDir, { recursive: true, force: true });\n if (status < 200 || status >= 300) throw new Error(`Fetch failed for ${url}: ${text || status}`);\n return text;\n}\n\nasync function fetchWithFallbackBinary(url) {\n try {\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n },\n });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return {\n buffer: Buffer.from(await response.arrayBuffer()),\n mime: response.headers.get(\"content-type\")?.split(\";\")[0] || \"image/png\",\n };\n } catch {\n return fetchBinaryWithPowerShell(url);\n }\n}\n\nasync function fetchBinaryWithPowerShell(url) {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"icon-agent-img-\"));\n const outputPath = path.join(tmpDir, \"image.bin\");\n const metaPath = path.join(tmpDir, \"meta.json\");\n const command = `\n$ErrorActionPreference = \"Stop\"\ntry {\n Add-Type -AssemblyName System.Net.Http\n $client = [System.Net.Http.HttpClient]::new()\n $client.Timeout = [TimeSpan]::FromSeconds(90)\n $client.DefaultRequestHeaders.UserAgent.ParseAdd(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n $response = $client.GetAsync(${psQuote(url)}).Result\n $bytes = $response.Content.ReadAsByteArrayAsync().Result\n [System.IO.File]::WriteAllBytes(${psQuote(outputPath)}, $bytes)\n $mime = \"image/png\"\n if ($response.Content.Headers.ContentType) { $mime = $response.Content.Headers.ContentType.MediaType }\n @{ status = [int]$response.StatusCode; mime = $mime } | ConvertTo-Json | Set-Content -LiteralPath ${psQuote(metaPath)} -Encoding UTF8\n if (-not $response.IsSuccessStatusCode) { exit 1 }\n} catch {\n @{ status = 500; mime = \"image/png\"; error = $_.Exception.Message } | ConvertTo-Json | Set-Content -LiteralPath ${psQuote(metaPath)} -Encoding UTF8\n exit 1\n}\n`;\n\n try {\n await execFileAsync(\"powershell\", [\"-NoProfile\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", command], {\n maxBuffer: 1024 * 1024 * 30,\n });\n } catch {\n // Inspect the meta file below.\n }\n\n const meta = fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath, \"utf8\").replace(/^\\uFEFF/, \"\")) : { status: 500 };\n if (!fs.existsSync(outputPath) || meta.status < 200 || meta.status >= 300) {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n throw new Error(meta.error || `Image download failed: HTTP ${meta.status}`);\n }\n const buffer = fs.readFileSync(outputPath);\n fs.rmSync(tmpDir, { recursive: true, force: true });\n return { buffer, mime: meta.mime || \"image/png\" };\n}\n\nfunction extractText(response) {\n if (typeof response.output_text === \"string\") return response.output_text;\n const chunks = [];\n for (const item of response.output || []) {\n for (const content of item.content || []) {\n if (content.type === \"output_text\" || content.type === \"text\") {\n chunks.push(content.text);\n }\n }\n }\n return chunks.join(\"\\n\").trim();\n}\n\nfunction parseJsonLoose(text) {\n try {\n return JSON.parse(text);\n } catch {\n const match = text.match(/\\{[\\s\\S]*\\}/);\n if (match) {\n try {\n return JSON.parse(match[0]);\n } catch {\n return null;\n }\n }\n return null;\n }\n}\n\nfunction htmlDecode(value = \"\") {\n return String(value)\n .replace(/\\\\u003d/g, \"=\")\n .replace(/\\\\u0026/g, \"&\")\n .replace(/\\\\u003c/g, \"<\")\n .replace(/\\\\u003e/g, \">\")\n .replace(/&amp;/g, \"&\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/\\\\n/g, \"\\n\");\n}\n\nfunction unique(values) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction imageInputItems(files = []) {\n return files\n .filter((file) => (file.publicUrl || file.dataUrl) && file.type?.startsWith(\"image/\"))\n .slice(0, 2)\n .map((file) => ({\n type: \"input_image\",\n image_url: file.dataUrl || localUploadDataUrlFromPublicUrl(file.publicUrl) || (String(file.publicUrl || \"\").includes(\"/uploads/\") ? \"\" : file.publicUrl),\n public_url: file.publicUrl || \"\",\n detail: \"low\",\n name: file.name || \"\",\n file_type: file.type || \"\",\n }))\n .filter((item) => item.image_url);\n}\n\nfunction mimeTypeFromFileName(filename = \"\") {\n const ext = path.extname(filename).toLowerCase();\n if (ext === \".jpg\" || ext === \".jpeg\") return \"image/jpeg\";\n if (ext === \".webp\") return \"image/webp\";\n if (ext === \".gif\") return \"image/gif\";\n if (ext === \".svg\") return \"image/svg+xml\";\n return \"image/png\";\n}\n\nfunction localUploadDataUrlFromPublicUrl(publicUrl = \"\") {\n if (!publicUrl) return \"\";\n let pathname = \"\";\n try {\n pathname = new URL(publicUrl, getPublicBaseUrl()).pathname;\n } catch {\n pathname = publicUrl;\n }\n if (!pathname.startsWith(\"/uploads/\")) return \"\";\n const filename = path.basename(decodeURIComponent(pathname));\n const resolved = path.resolve(uploadDir, filename);\n if (!resolved.startsWith(path.resolve(uploadDir) + path.sep) || !fs.existsSync(resolved)) return \"\";\n const buffer = fs.readFileSync(resolved);\n return `data:${mimeTypeFromFileName(filename)};base64,${buffer.toString(\"base64\")}`;\n}\n\nasync function visualReferenceInputItems(input) {\n const uploaded = imageInputItems(input.referenceFiles);\n const refs = getGooglePlayReferences(input);\n const competitorRefs = getCompetitorGooglePlayReferences(input);\n const googlePlayUrls = [\n refs.icon,\n refs.featureGraphic,\n ...(refs.screenshots || []).slice(0, 4),\n ...competitorRefs.map((item) => item.icon),\n ].filter(Boolean);\n\n if (aiProvider === \"deepseek\") {\n const googlePlay = googlePlayUrls.map((url) => ({\n type: \"input_image\",\n image_url: url,\n public_url: url,\n detail: \"low\",\n }));\n return [...uploaded, ...googlePlay].slice(0, 8);\n }\n\n const googlePlay = [];\n for (const url of googlePlayUrls) {\n try {\n googlePlay.push({\n type: \"input_image\",\n image_url: await imageUrlToDataUrl(url),\n public_url: url,\n detail: \"low\",\n });\n } catch (error) {\n console.warn(`Failed to inline Google Play image: ${url} ${error.message}`);\n }\n }\n return [...uploaded, ...googlePlay].slice(0, 8);\n}\n\nasync function generationReferenceInputItems(input) {\n const uploaded = imageInputItems(input.referenceFiles).map((item, index) => ({\n image: item.image_url,\n source: `用户上传参考图 ${index + 1}`,\n }));\n const refs = getGooglePlayReferences(input);\n const competitorRefs = getCompetitorGooglePlayReferences(input);\n const googlePlayUrls = [\n refs.icon ? { url: refs.icon, source: \"主产品 Google Play icon\" } : null,\n ...competitorRefs.map((item, index) => (item.icon ? { url: item.icon, source: `竞品 Google Play icon ${index + 1}` } : null)),\n ].filter(Boolean);\n\n const googlePlay = [];\n const errors = [];\n for (const item of googlePlayUrls) {\n try {\n googlePlay.push({\n image: await imageUrlToDataUrl(normalizeGoogleImageUrl(item.url)),\n source: item.source,\n });\n } catch (error) {\n errors.push({ url: item.url, source: item.source, error: error.message });\n console.warn(`Failed to inline generation reference image: ${item.url} ${error.message}`);\n }\n }\n\n const refsWithSources = [...uploaded, ...googlePlay].slice(0, 5);\n return {\n images: refsWithSources.filter((item) => item.image).map((item) => item.image),\n sources: refsWithSources.filter((item) => item.image).map((item) => item.source),\n errors,\n };\n}\n\nfunction getCompetitorGooglePlayReferences(input) {\n const packs =\n input.competitorGooglePlayReferences ||\n input.competitor_google_play_references ||\n input.competitor_visual_reference_pack ||\n input.googlePlay?.competitor_visual_reference_pack ||\n [];\n return packs.map((item) => ({\n icon: item.icon || item.app_icon || \"\",\n }));\n}\n\nfunction normalizeGoogleImageUrl(url) {\n if (!url || !url.includes(\"play-lh.googleusercontent.com\")) return url;\n if (/=[whs]\\d+/.test(url)) return url;\n return `${url}=w1024-h1024-rw`;\n}\n\nfunction getGooglePlayReferences(input) {\n const refs =\n input.googlePlayReferences ||\n input.google_play_visual_reference_pack ||\n input.visual_reference_pack ||\n input.googlePlay?.visual_reference_pack ||\n {};\n return {\n icon: refs.icon || refs.app_icon || \"\",\n featureGraphic: refs.featureGraphic || refs.feature_graphic || refs.feature || \"\",\n screenshots: refs.screenshots || refs.screen_shots || [],\n };\n}\n\nasync function imageUrlToDataUrl(url) {\n const response = await fetchWithFallbackBinary(url);\n const mime = response.mime || \"image/png\";\n return `data:${mime};base64,${response.buffer.toString(\"base64\")}`;\n}\n\nasync function runStructuredAnalysis({ text, images = [] }) {\n if (aiProvider === \"volcengine\") {\n const content = [\n { type: \"text\", text },\n ...images.map((image) => ({\n type: \"image_url\",\n image_url: { url: image.image_url },\n })),\n ];\n\n const response = await arkFetch(\"chat/completions\", {\n model: volcengineTextModel,\n messages: [{ role: \"user\", content }],\n temperature: 0.2,\n });\n\n return response.choices?.[0]?.message?.content || \"\";\n }\n\n if (aiProvider === \"aliyun\") {\n const content = [\n { type: \"text\", text },\n ...images.map((image) => ({\n type: \"image_url\",\n image_url: { url: image.image_url },\n })),\n ];\n\n const response = await dashscopeFetch(\"services/aigc/text-generation/generation\", {\n model: aliyunTextModel,\n input: { messages: [{ role: \"user\", content }] },\n parameters: { temperature: 0.2 },\n });\n\n return response.output?.text || response.output?.choices?.[0]?.message?.content || \"\";\n }\n\n if (aiProvider === \"kimi\") {\n const content = [\n { type: \"text\", text },\n ...images.map((image) => ({\n type: \"image_url\",\n image_url: { url: image.image_url },\n })),\n ];\n\n const response = await kimiFetch(\"chat/completions\", {\n model: kimiTextModel,\n messages: [{ role: \"user\", content }],\n temperature: 0.2,\n });\n\n return response.choices?.[0]?.message?.content || \"\";\n }\n\n if (aiProvider === \"deepseek\") {\n const { publicImages, skippedImages } = await deepseekImageInputs(images);\n const imageNotes = skippedImages.length\n ? `\\n\\nSome reference images are not passed as image_url because they are not stable public direct image URLs. Use these source notes as reference context, and do not claim pixel-level certainty for them:\\n${skippedImages\n .map((note, index) => `- Skipped reference ${index + 1}: ${note}`)\n .join(\"\\n\")}`\n : \"\";\n const content = [\n { type: \"text\", text: `${text}${imageNotes}` },\n ...publicImages.map((url) => ({\n type: \"image_url\",\n image_url: { url },\n })),\n ];\n\n let response;\n try {\n response = await deepseekFetch(\"chat/completions\", {\n model: deepseekTextModel,\n messages: [{ role: \"user\", content: publicImages.length ? content : `${text}${imageNotes}` }],\n temperature: 0.2,\n });\n } catch (error) {\n if (!publicImages.length || !/image_url|unknown variant|expected `text`|expected text/i.test(error.message)) throw error;\n response = await deepseekFetch(\"chat/completions\", {\n model: deepseekTextModel,\n messages: [\n {\n role: \"user\",\n content: `${text}\\n\\nImage URL inputs were validated as public direct image URLs, but the current DeepSeek endpoint rejected image_url content. Fall back to text-only analysis using these reference URLs as context:\\n${publicImages\n .map((url, index) => `- Reference ${index + 1}: ${url}`)\n .join(\"\\n\")}${imageNotes}`,\n },\n ],\n temperature: 0.2,\n });\n }\n\n return response.choices?.[0]?.message?.content || \"\";\n }\n\n const response = await openaiFetch(\"responses\", {\n model: textModel,\n input: [\n {\n role: \"user\",\n content: [{ type: \"input_text\", text }, ...images],\n },\n ],\n });\n\n return extractText(response);\n}\n\nfunction referenceImageNote(image = {}) {\n const value = image.image_url || image.url || image.name || \"uploaded image\";\n if (/^data:/i.test(value)) {\n const mime = value.slice(5, value.indexOf(\";\") > 0 ? value.indexOf(\";\") : 32);\n return `uploaded ${mime || \"image\"} reference`;\n }\n return String(value).slice(0, 240);\n}\n\nasync function deepseekImageInputs(images = []) {\n const publicImages = [];\n const skippedImages = [];\n for (const image of images.slice(0, 8)) {\n const candidate = image.public_url || image.image_url || image.url || \"\";\n if (!isPublicHttpUrl(candidate)) {\n skippedImages.push(`${referenceImageNote(image)}; not a public HTTP(S) URL`);\n continue;\n }\n const validation = await validatePublicImageUrl(candidate);\n if (validation.ok) {\n publicImages.push(candidate);\n } else {\n skippedImages.push(`${candidate}; ${validation.reason}`);\n }\n }\n return { publicImages, skippedImages };\n}\n\nfunction isPublicHttpUrl(value = \"\") {\n try {\n const url = new URL(value);\n if (!/^https?:$/.test(url.protocol)) return false;\n const hostname = url.hostname.toLowerCase();\n if (\n hostname === \"localhost\" ||\n hostname === \"127.0.0.1\" ||\n hostname === \"::1\" ||\n hostname.endsWith(\".local\") ||\n /^10\\./.test(hostname) ||\n /^192\\.168\\./.test(hostname) ||\n /^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(hostname)\n ) {\n return false;\n }\n return true;\n } catch {\n return false;\n }\n}\n\nasync function validatePublicImageUrl(url) {\n const check = async (method) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 10000);\n try {\n const response = await fetch(url, {\n method,\n headers: method === \"GET\" ? { Range: \"bytes=0-0\" } : undefined,\n signal: controller.signal,\n });\n const contentType = response.headers.get(\"content-type\") || \"\";\n if (response.ok && /^image\\//i.test(contentType)) return { ok: true, contentType };\n return { ok: false, reason: `Content-Type is \"${contentType || \"missing\"}\"` };\n } finally {\n clearTimeout(timer);\n }\n };\n\n try {\n const head = await check(\"HEAD\");\n if (head.ok) return head;\n } catch (error) {\n // Some image CDNs reject HEAD; try a tiny ranged GET below.\n }\n\n try {\n return await check(\"GET\");\n } catch (error) {\n return { ok: false, reason: error.name === \"AbortError\" ? \"validation timed out\" : error.message };\n }\n}\n\nfunction buildWorkflowPayload(input) {\n return {\n product: input.product || \"未填写\",\n competitors: input.competitors || \"无\",\n platform: input.platform || \"Google Ads\",\n imported_platform_rules: input.platformRules || \"\",\n emotion_target: input.emotion || [],\n text_spec:\n input.textEnabled === \"是\"\n ? { enabled: true, text: input.badgeText || \"\", position: \"右下角角标\", max_chars: 5 }\n : { enabled: false, no_text: true },\n emphasis: input.emphasis || \"\",\n sensitive: input.sensitive || \"\",\n reference_notes: input.reference || \"\",\n uploaded_files: (input.referenceFiles || []).slice(0, 2).map((file) => ({\n name: file.name,\n type: file.type,\n size: file.size,\n })),\n google_play_profile: input.googlePlayProfile || null,\n google_play_visual_reference_pack: input.googlePlayReferences || null,\n competitor_google_play_profiles: input.competitorGooglePlayProfiles || [],\n competitor_google_play_icons: (input.competitorGooglePlayReferences || []).map((item) => ({\n app_id: item.app_id || \"\",\n title: item.title || item.app_title || \"\",\n icon: item.icon || \"\",\n })),\n variant_count: Number(input.count || 2),\n directions: input.directions || [\"点击强化\"],\n };\n}\n\nasync function analyzeWorkflow(input) {\n const workflow = buildWorkflowPayload(input);\n if (appConfig.prompt_templates?.s2_analysis) {\n const configuredPrompt = renderTemplate(appConfig.prompt_templates.s2_analysis, {\n WORKFLOW_JSON: workflow,\n PROTOCOL_SCHEMA_JSON: appConfig.protocol_schema || {},\n OUTPUT_SCHEMA_JSON: appConfig.output_schemas?.s2_analysis || {},\n PROMPT_FIELD_NAMES_JSON: appConfig.prompt_field_names || [],\n });\n const text = await runStructuredAnalysis({\n text: configuredPrompt,\n images: await visualReferenceInputItems(input),\n });\n return normalizeAnalysisResult(parseJsonLoose(text) || { raw_text: text });\n }\n const text = await runStructuredAnalysis({\n text: `你是广告投放 Icon 素材生成 Agent 的 S2 分析器。\n你的任务:基于 S1 输入、Google Play 主产品资料、主产品 icon / feature graphic / screenshots、竞品 icon、用户上传参考图,输出产品/竞品/icon 的结构化分析。\n注意:平台约束来自 imported_platform_rules,由前端可编辑规则导入维护。你不要分析、改写或生成平台政策约束。\n\n只返回 JSON,不要 Markdown,不要解释。\n\n必须输出以下 JSON 结构,字段缺失时也要给出合理推断,不要留空:\n{\n \"product_analysis\": {\n \"core_gameplay\": \"\",\n \"theme\": \"\",\n \"art_style\": \"\",\n \"visual_style\": \"\",\n \"target_audience\": \"\",\n \"core_selling_points\": [],\n \"ad_expressible_points\": []\n },\n \"product_icon_analysis\": {\n \"image_content\": \"\",\n \"main_subject\": \"\",\n \"visual_style\": \"\",\n \"color_features\": \"\",\n \"composition_features\": \"\",\n \"text_features\": \"\",\n \"small_size_readability\": \"\",\n \"identity_cues\": []\n },\n \"competitor_icon_analysis\": [\n {\n \"app_title\": \"\",\n \"app_id\": \"\",\n \"image_content\": \"\",\n \"main_subject\": \"\",\n \"visual_style\": \"\",\n \"color_features\": \"\",\n \"composition_features\": \"\",\n \"text_features\": \"\",\n \"small_size_readability\": \"\",\n \"notable_click_drivers\": []\n }\n ],\n \"common_icon_signature\": {\n \"subject_pattern\": \"\",\n \"composition_pattern\": \"\",\n \"color_pattern\": \"\",\n \"rendering_pattern\": \"\",\n \"emotion_pattern\": \"\",\n \"text_or_badge_pattern\": \"\",\n \"market_effective_formula\": []\n },\n \"differentiation_opportunities\": {\n \"keep\": [],\n \"strengthen\": [],\n \"avoid_copying\": [],\n \"possible_breakthrough_points\": []\n },\n \"generation_prompt_fields\": {\n \"game_genre\": \"\",\n \"setting\": \"\",\n \"core_narrative\": \"\",\n \"scene_context\": \"\",\n \"subject_type\": \"\",\n \"archetype\": \"\",\n \"pose\": \"\",\n \"facial_expression\": \"\",\n \"camera_angle\": \"\",\n \"eye_contact\": \"\",\n \"render_style\": \"\",\n \"detail_level\": \"\",\n \"lighting_style\": \"\",\n \"color_strategy\": \"\",\n \"surface_finish\": \"\",\n \"background_story\": \"\",\n \"curiosity_trigger\": \"\",\n \"urgency_trigger\": \"\",\n \"emotional_reaction_trigger\": \"\",\n \"dramatic_visual_tension\": \"\",\n \"value_desire\": \"\",\n \"symbol_set\": \"\"\n },\n \"locked_insights_for_next_stage\": {\n \"identity_anchor\": \"\",\n \"reference_priority\": [\"user_uploaded_reference\", \"product_icon\", \"competitor_icons\"],\n \"must_include_candidates\": [],\n \"must_not_include_candidates\": [],\n \"style_keywords\": [],\n \"risk_flags\": []\n }\n}\n\n生成规则:\n1. S2 只做理解与分析,不生成图片,不输出生成 prompt。\n2. 必须分别分析主产品 icon 和竞品 icon。\n3. 不要输出 platform_constraints,不要判断平台政策,不要改写 imported_platform_rules。\n4. generation_prompt_fields 的字段名必须与 S3 提示词模板占位符保持一致;内容必须从 S1 输入、Google Play 信息、图片理解和竞品 icon 分析中提炼,不要留空。\n5. reference_priority 必须固定为:user_uploaded_reference > product_icon > competitor_icons。\n6. 不要输出 seed,不要输出内部规则。\n\n输入:\n${JSON.stringify(workflow, null, 2)}`,\n images: await visualReferenceInputItems(input),\n });\n\n return parseJsonLoose(text) || { raw_text: text };\n}\n\nasync function optimizePrompts(input) {\n const workflow = buildWorkflowPayload(input);\n const count = Math.max(1, Math.min(2, Number(input.count || workflow.variant_count || 2)));\n const directions = (input.directions || workflow.directions || [\"点击强化\"]).slice(0, 2);\n const text = await runStructuredAnalysis({\n text: `你是移动游戏广告 Icon 的高级提示词创意总监。\n任务:不要直接生成最终 prompt,而是基于 S1/S2 信息,为每个方案生成可填入固定模板的 generation_prompt_fields。\n\n只返回 JSON,不要 Markdown,不要解释。输出结构:\n{\n \"prompt_plan\": [\n {\n \"variant_tag\": \"保守/点击强化/极致夸张\",\n \"creative_rationale\": \"\",\n \"generation_prompt_fields\": {\n \"game_genre\": \"\",\n \"setting\": \"\",\n \"core_narrative\": \"\",\n \"scene_context\": \"\",\n \"subject_type\": \"\",\n \"archetype\": \"\",\n \"pose\": \"\",\n \"facial_expression\": \"\",\n \"camera_angle\": \"\",\n \"eye_contact\": \"\",\n \"render_style\": \"\",\n \"detail_level\": \"\",\n \"lighting_style\": \"\",\n \"color_strategy\": \"\",\n \"surface_finish\": \"\",\n \"background_story\": \"\",\n \"curiosity_trigger\": \"\",\n \"urgency_trigger\": \"\",\n \"emotional_reaction_trigger\": \"\",\n \"dramatic_visual_tension\": \"\",\n \"value_desire\": \"\",\n \"symbol_set\": \"\"\n }\n }\n ]\n}\n\n硬规则:\n1. prompt_plan 数量必须等于 ${count}。\n2. 每个 variant_tag 必须来自用户选择方向:${directions.join(\" / \")}。\n3. 各方案必须明显不同:主体、姿态、光影、色彩策略或点击机制至少 3 项不同。\n4. 必须把 S1/S2 的产品信息、icon 分析、竞品共性、情绪目标转译成具体画面语言。\n5. 不要输出平台政策,平台约束由模板 STRICT CONSTRAINTS 注入。\n6. 不要输出 seed,不要输出最终整段 prompt。\n\n输入:\n${JSON.stringify(\n {\n workflow,\n model_analysis: input.modelAnalysis || null,\n reference_summary: {\n has_user_uploads: Boolean((input.referenceFiles || []).length),\n has_product_icon: Boolean(input.googlePlayReferences?.icon),\n competitor_icon_count: (input.competitorGooglePlayReferences || []).filter((item) => item.icon).length,\n },\n },\n null,\n 2,\n)}`,\n });\n const parsed = parseJsonLoose(text);\n if (parsed?.prompt_plan) return parsed.prompt_plan.slice(0, count);\n return [];\n}\n\nfunction normalizeAnalysisResult(result = {}) {\n const promptFields = appConfig.prompt_field_names || [];\n result.ICON_CREATIVE_PROTOCOL = result.ICON_CREATIVE_PROTOCOL || appConfig.protocol_schema?.ICON_CREATIVE_PROTOCOL || {};\n result.generation_prompt_fields = result.generation_prompt_fields || {};\n for (const field of promptFields) {\n if (result.generation_prompt_fields[field] === undefined) result.generation_prompt_fields[field] = \"\";\n }\n result.locked_insights_for_next_stage = result.locked_insights_for_next_stage || {};\n result.locked_insights_for_next_stage.reference_priority =\n result.locked_insights_for_next_stage.reference_priority || appConfig.reference_policy?.priority || [\"user_uploaded_reference\", \"product_icon\", \"competitor_icons\"];\n return result;\n}\n\nfunction normalizePromptPlan(plan = [], directions = [], count = 1) {\n const promptFields = appConfig.prompt_field_names || [];\n return Array.from({ length: count }).map((_, index) => {\n const item = plan[index] || {};\n const fields = item.generation_prompt_fields || {};\n for (const field of promptFields) {\n if (fields[field] === undefined) fields[field] = \"\";\n }\n return {\n variant_tag: item.variant_tag || directions[index % Math.max(1, directions.length)] || \"点击强化\",\n creative_rationale: item.creative_rationale || \"\",\n generation_mode: item.generation_mode || \"\",\n reference_image_policy: item.reference_image_policy || {\n use_user_uploaded_references: true,\n use_product_icon_reference: true,\n use_competitor_icon_references: true,\n reference_priority: appConfig.reference_policy?.priority || [\"user_uploaded_reference\", \"product_icon\", \"competitor_icons\"],\n },\n generation_prompt_fields: fields,\n final_prompt: item.final_prompt || item.prompt || item.generation_prompt || \"\",\n prompt: item.final_prompt || item.prompt || item.generation_prompt || \"\",\n negative_constraints: item.negative_constraints || [],\n quality_checkpoints: item.quality_checkpoints || [],\n };\n });\n}\n\nasync function optimizePromptsConfigured(input) {\n if (!appConfig.prompt_templates?.s4_prompt_plan) return optimizePrompts(input);\n const workflow = buildWorkflowPayload(input);\n const count = Math.max(1, Math.min(2, Number(input.count || workflow.variant_count || 2)));\n const directions = (input.directions || workflow.directions || [\"点击强化\"]).slice(0, 2);\n const s4Input = {\n workflow,\n model_analysis: input.modelAnalysis || null,\n icon_creative_protocol: input.modelAnalysis?.ICON_CREATIVE_PROTOCOL || null,\n selected_directions: directions,\n variant_count: count,\n reference_summary: {\n has_user_uploads: Boolean((input.referenceFiles || []).length),\n uploaded_reference_public_urls: (input.referenceFiles || []).map((file) => file.publicUrl || file.url || \"\").filter(Boolean),\n has_product_icon: Boolean(input.googlePlayReferences?.icon),\n product_icon_url: input.googlePlayReferences?.icon || \"\",\n competitor_icon_count: (input.competitorGooglePlayReferences || []).filter((item) => item.icon).length,\n competitor_icon_urls: (input.competitorGooglePlayReferences || []).map((item) => item.icon).filter(Boolean),\n },\n };\n const configuredPrompt = renderTemplate(appConfig.prompt_templates.s4_prompt_plan, {\n S4_INPUT_JSON: s4Input,\n OUTPUT_SCHEMA_JSON: appConfig.output_schemas?.s4_prompt_plan || {},\n PROMPT_FIELD_NAMES_JSON: appConfig.prompt_field_names || [],\n COUNT: String(count),\n DIRECTIONS: directions.join(\" / \"),\n });\n const text = await runStructuredAnalysis({ text: configuredPrompt });\n const parsed = parseJsonLoose(text);\n if (parsed?.prompt_plan) return normalizePromptPlan(parsed.prompt_plan, directions, count);\n return [];\n}\n\nasync function checkPrompts(input) {\n const promptJson = Array.isArray(input.promptJson) ? input.promptJson : [];\n if (!promptJson.length) throw new Error(\"No prompt_json found for S4 prompt check.\");\n const checkInput = {\n platform: input.platform || \"Google Ads\",\n product: input.product || \"\",\n prompt_count: promptJson.length,\n prompt_items: promptJson.map((prompt, index) => ({\n prompt_id: prompt.prompt_id || `prompt_${index + 1}`,\n variant_tag: prompt.variant_tag || \"\",\n prompt_text: prompt.prompt_text || prompt.prompt || \"\",\n must_include: prompt.constraints?.must_include || [],\n must_not_include: prompt.constraints?.must_not_include || [],\n text_spec: prompt.text_spec || {},\n })),\n };\n const configuredPrompt = renderTemplate(appConfig.prompt_templates?.s4_prompt_check || \"\", {\n PROMPT_CHECK_INPUT_JSON: checkInput,\n OUTPUT_SCHEMA_JSON: appConfig.output_schemas?.s4_prompt_check || {},\n });\n const text = await runStructuredAnalysis({ text: configuredPrompt || JSON.stringify(checkInput, null, 2) });\n const parsed = parseJsonLoose(text) || { raw_text: text };\n return normalizePromptCheck(parsed, promptJson);\n}\n\nfunction normalizePromptCheck(result = {}, promptJson = []) {\n const checkedItems = Array.isArray(result.checked_items) ? result.checked_items : [];\n const normalizedItems = promptJson.map((prompt, index) => {\n const item = checkedItems[index] || checkedItems.find((entry) => entry.prompt_id === prompt.prompt_id) || {};\n return {\n prompt_id: item.prompt_id || prompt.prompt_id || `prompt_${index + 1}`,\n variant_tag: item.variant_tag || prompt.variant_tag || \"\",\n status: [\"pass\", \"warning\", \"block\"].includes(item.status) ? item.status : \"warning\",\n risk_level: item.risk_level || (item.status === \"block\" ? \"high\" : item.status === \"warning\" ? \"medium\" : \"none\"),\n risk_categories: Array.isArray(item.risk_categories) ? item.risk_categories : [],\n flagged_terms: Array.isArray(item.flagged_terms) ? item.flagged_terms : [],\n reason: item.reason || \"\",\n rewrite_suggestions: Array.isArray(item.rewrite_suggestions) ? item.rewrite_suggestions : [],\n };\n });\n const hasBlock = normalizedItems.some((item) => item.status === \"block\");\n const hasWarning = normalizedItems.some((item) => item.status === \"warning\");\n return {\n overall_status: hasBlock ? \"block\" : hasWarning ? \"warning\" : result.overall_status || \"pass\",\n summary: result.summary || (hasBlock ? \"存在高风险提示词,建议修改后再生成。\" : hasWarning ? \"存在可能触发过滤的措辞,建议优化。\" : \"未发现明显高风险禁用词。\"),\n checked_items: normalizedItems,\n global_rewrite_suggestions: Array.isArray(result.global_rewrite_suggestions) ? result.global_rewrite_suggestions : [],\n };\n}\n\nasync function googlePlayLookup(input) {\n const product = String(input.product || \"\").trim();\n const competitors = String(input.competitors || \"\")\n .split(/[,\\n,、]/)\n .map((item) => item.trim())\n .filter(Boolean)\n .slice(0, 3);\n\n if (!product) throw new Error(\"请先填写待投产品名称。\");\n\n const apps = [];\n apps.push(await googlePlayLookupOne(product, \"product\"));\n for (const competitor of competitors) {\n apps.push(await googlePlayLookupOne(competitor, \"competitor\"));\n }\n\n const primary = apps[0];\n return {\n product_profile: primary.profile,\n competitor_profiles: apps.slice(1).map((app) => app.profile),\n visual_reference_pack: primary.visual_reference_pack,\n competitor_visual_reference_pack: apps.slice(1).map((app) => ({\n app_id: app.profile.app_id,\n title: app.profile.app_title,\n icon: app.visual_reference_pack.icon,\n screenshots: app.visual_reference_pack.screenshots?.slice(0, 4) || [],\n })),\n source_apps: apps,\n };\n}\n\nasync function googlePlayLookupOne(query, role) {\n const appId = query.includes(\".\") && !/\\s/.test(query) ? query : await findGooglePlayAppId(query);\n const detailUrl = `https://play.google.com/store/apps/details?id=${encodeURIComponent(appId)}&hl=en&gl=US`;\n const html = await fetchTextWithFallback(detailUrl);\n return parseGooglePlayDetail(html, appId, role, detailUrl);\n}\n\nasync function findGooglePlayAppId(query) {\n const searchUrl = `https://play.google.com/store/search?q=${encodeURIComponent(query)}&c=apps&hl=en&gl=US`;\n const html = await fetchTextWithFallback(searchUrl);\n const ids = unique(\n [...html.matchAll(/\\/store\\/apps\\/details\\?id\\\\?=([a-zA-Z0-9._]+)/g)].map((match) =>\n htmlDecode(match[1]).replace(/\\\\u0026.*$/, \"\"),\n ),\n );\n if (!ids.length) throw new Error(`Google Play 未找到应用:${query}`);\n return ids[0];\n}\n\nfunction parseGooglePlayDetail(html, appId, role, detailUrl) {\n const title =\n matchMeta(html, \"og:title\")?.replace(/\\s*-\\s*(Android\\s+)?Apps on Google Play$/i, \"\") ||\n matchTag(html, \"title\")?.replace(/\\s*-\\s*(Android\\s+)?Apps on Google Play$/i, \"\") ||\n appId;\n const shortDescription = matchMeta(html, \"og:description\") || \"\";\n const developer = extractNearAppIdDeveloper(html, appId) || \"\";\n const installs = firstMatch(html, /\"([0-9,]+\\+)\"\\s*,null,\"\"\\s*,null,null,null,null,\\[null,2,\\[500,1024\\]/) || \"\";\n const category = firstMatch(html, /\\],\"([^\"]+)\",null,null,\\[null,\\[\\[0,\"USD\"/) || \"\";\n const rating = firstMatch(html, /\\[\"([0-9.]+)\",[0-9.]+\\],\"[^\"]+\"/) || \"\";\n const media = extractGooglePlayMedia(html);\n const icon = matchMeta(html, \"og:image\") || media.find((item) => item.width === 512 && item.height === 512)?.url || media[0]?.url || \"\";\n const screenshots = media\n .filter((item) => item.url !== icon && item.width >= 700 && item.height >= 700)\n .sort((a, b) => b.width * b.height - a.width * a.height)\n .map((item) => item.url)\n .slice(0, 8);\n const featureGraphic =\n media.find((item) => item.url !== icon && item.width >= 900 && item.height >= 450 && item.width > item.height)?.url ||\n screenshots.find((url) => url) ||\n \"\";\n const cleanScreenshots = screenshots.filter((url) => url !== featureGraphic).slice(0, 8);\n\n return {\n profile: {\n role,\n app_id: appId,\n app_title: htmlDecode(title),\n developer: htmlDecode(developer),\n category: htmlDecode(category),\n rating,\n installs,\n short_description: htmlDecode(shortDescription),\n detail_url: detailUrl,\n },\n visual_reference_pack: {\n icon,\n featureGraphic,\n screenshots: cleanScreenshots,\n visual_rules: summarizeVisualRules(media, icon, cleanScreenshots),\n },\n };\n}\n\nfunction extractGooglePlayMedia(html) {\n const media = [];\n const regex = /\\[null,2,\\[(\\d+),(\\d+)\\],\\[null,null,\"(https:\\/\\/play-lh\\.googleusercontent\\.com\\/[^\"]+)\"/g;\n let match;\n while ((match = regex.exec(html))) {\n media.push({\n width: Number(match[1]),\n height: Number(match[2]),\n url: htmlDecode(match[3]),\n });\n }\n const metaImage = matchMeta(html, \"og:image\");\n if (metaImage) media.unshift({ width: 512, height: 512, url: metaImage });\n const seen = new Set();\n return media.filter((item) => {\n if (seen.has(item.url)) return false;\n seen.add(item.url);\n return true;\n });\n}\n\nfunction summarizeVisualRules(media, icon, screenshots) {\n const iconItem = media.find((item) => item.url === icon);\n const landscape = media.filter((item) => item.width > item.height).length;\n const portrait = media.filter((item) => item.height > item.width).length;\n return [\n iconItem ? `icon 素材尺寸约 ${iconItem.width}x${iconItem.height}` : \"已抓取 icon 默认参考图\",\n screenshots.length ? `抓取到 ${screenshots.length} 张截图/主图素材` : \"截图素材较少,需依赖 icon 和用户素材\",\n landscape >= portrait ? \"视觉素材以横版展示为主\" : \"视觉素材以竖版展示为主\",\n \"图片来源为 Google Play 公开素材,仅作为风格与构图参考\",\n ];\n}\n\nfunction matchMeta(html, property) {\n const escaped = property.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return htmlDecode(\n firstMatch(html, new RegExp(`<meta[^>]+(?:property|name)=[\"']${escaped}[\"'][^>]+content=[\"']([^\"']+)[\"']`, \"i\")) ||\n firstMatch(html, new RegExp(`<meta[^>]+content=[\"']([^\"']+)[\"'][^>]+(?:property|name)=[\"']${escaped}[\"']`, \"i\")) ||\n \"\",\n );\n}\n\nfunction matchTag(html, tag) {\n return htmlDecode(firstMatch(html, new RegExp(`<${tag}[^>]*>([\\\\s\\\\S]*?)<\\\\/${tag}>`, \"i\")) || \"\");\n}\n\nfunction firstMatch(text, regex) {\n const match = text.match(regex);\n return match ? match[1] : \"\";\n}\n\nfunction extractNearAppIdDeveloper(html, appId) {\n const index = html.indexOf(`\"${appId}\"`);\n if (index === -1) return \"\";\n const slice = html.slice(index, index + 20000);\n return firstMatch(slice, /],\"([^\"]+)\",\\[\"[0-9.]+\",[0-9.]+\\],\"[^\"]+\"/) || \"\";\n}\n\nasync function makeSmokeReferenceDataUrl() {\n const buffer = await sharp({\n create: {\n width: 32,\n height: 32,\n channels: 4,\n background: \"#2563eb\",\n },\n })\n .composite([\n {\n input: Buffer.from('<svg width=\"32\" height=\"32\"><circle cx=\"16\" cy=\"16\" r=\"9\" fill=\"#facc15\"/></svg>'),\n top: 0,\n left: 0,\n },\n ])\n .png()\n .toBuffer();\n return `data:image/png;base64,${buffer.toString(\"base64\")}`;\n}\n\nfunction smokeBasePayload(uploaded, dataUrl = \"\") {\n return {\n product: \"Test Kingdom Builder\",\n competitors: \"Township\",\n platform: \"Google Ads\",\n platformRules: \"- No misleading claims\\n- No gore, nudity, or copyrighted competitor logos\\n- Icon must be readable at 64px\",\n emotion: [\"好奇\", \"成就感\"],\n textEnabled: \"否\",\n badgeText: \"\",\n reference: \"Use uploaded reference as highest priority style cue.\",\n referenceFiles: uploaded\n ? [\n {\n name: uploaded.name,\n type: uploaded.type,\n size: uploaded.size,\n url: uploaded.url,\n publicUrl: uploaded.public_url,\n dataUrl,\n },\n ]\n : [],\n googlePlayProfile: {\n app_title: \"Test Kingdom Builder\",\n app_id: \"test.kingdom.builder\",\n category: \"Simulation\",\n developer: \"Test Studio\",\n short_description: \"Build a medieval town, collect resources, and unlock rewards.\",\n },\n googlePlayReferences: {\n icon: uploaded?.public_url || \"\",\n featureGraphic: \"\",\n screenshots: [],\n },\n competitorGooglePlayProfiles: [\n {\n app_title: \"Township\",\n app_id: \"com.playrix.township\",\n category: \"Simulation\",\n },\n ],\n competitorGooglePlayReferences: [],\n count: 1,\n directions: [\"点击强化\"],\n promptJson: [],\n generatedImages: [],\n };\n}\n\nfunction summarizeForSmoke(value, maxChars = 7000) {\n const text = JSON.stringify(value, null, 2);\n return text.length > maxChars ? { truncated: true, json_preview: text.slice(0, maxChars) } : value;\n}\n\nasync function runSmokeTest(mode, req) {\n const normalizedMode = [\"quick\", \"ai\", \"full\"].includes(mode) ? mode : \"quick\";\n const startedAt = new Date().toISOString();\n const steps = [];\n const io = {};\n const mark = (name, status, detail = {}) => steps.push({ name, status, ...detail });\n\n mark(\"静态页面\", \"passed\", { message: \"index.html 由当前服务托管\" });\n mark(\"模型配置\", providerHasKey(aiProvider) && providerHasKey(imageProvider) ? \"passed\" : \"warning\", {\n text_provider: aiProvider,\n text_model: aiProvider === \"deepseek\" ? deepseekTextModel : textModel,\n image_provider: imageProvider,\n image_model: imageProvider === \"volcengine\" ? volcengineImageModel : imageModel,\n });\n mark(\"配置文件\", appConfig.protocol_schema?.ICON_CREATIVE_PROTOCOL && appConfig.prompt_templates?.s2_analysis ? \"passed\" : \"failed\", {\n has_protocol_schema: Boolean(appConfig.protocol_schema?.ICON_CREATIVE_PROTOCOL),\n has_s2_template: Boolean(appConfig.prompt_templates?.s2_analysis),\n has_s4_template: Boolean(appConfig.prompt_templates?.s4_prompt_plan),\n });\n\n const uploadInput = {\n name: \"smoke-reference.png\",\n type: \"image/png\",\n dataUrl: await makeSmokeReferenceDataUrl(),\n };\n const uploaded = await uploadReferenceImage(uploadInput, req);\n mark(\"参考图上传\", \"passed\", {\n public_url: uploaded.public_url,\n type: uploaded.type,\n size: uploaded.size,\n });\n\n const payload = smokeBasePayload(uploaded, uploadInput.dataUrl);\n io.quick = {\n upload_input: { name: uploadInput.name, type: uploadInput.type, data_url_length: uploadInput.dataUrl.length },\n upload_output: uploaded,\n };\n\n let analysis = null;\n let promptPlan = null;\n if (normalizedMode === \"ai\" || normalizedMode === \"full\") {\n io.analyze = { input: payload };\n analysis = await analyzeWorkflow(payload);\n io.analyze.output = summarizeForSmoke(analysis);\n mark(\"S2 AI 分析\", analysis?.generation_prompt_fields ? \"passed\" : \"failed\", {\n has_protocol: Boolean(analysis?.ICON_CREATIVE_PROTOCOL),\n has_generation_prompt_fields: Boolean(analysis?.generation_prompt_fields),\n });\n\n const optimizeInput = { ...payload, modelAnalysis: analysis };\n io.prompt_plan = { input: optimizeInput };\n promptPlan = await optimizePromptsConfigured(optimizeInput);\n io.prompt_plan.output = summarizeForSmoke(promptPlan);\n mark(\"S4 提示词计划\", Array.isArray(promptPlan) && promptPlan.length ? \"passed\" : \"failed\", {\n prompt_count: Array.isArray(promptPlan) ? promptPlan.length : 0,\n has_final_prompt: Boolean(promptPlan?.[0]?.final_prompt || promptPlan?.[0]?.prompt),\n });\n }\n\n if (normalizedMode === \"full\") {\n const promptText =\n promptPlan?.[0]?.final_prompt ||\n promptPlan?.[0]?.prompt ||\n \"Create a production-ready transparent-background mobile game app icon, single golden castle emblem, high contrast, readable at 64px.\";\n const generateInput = {\n ...payload,\n promptJson: [\n {\n prompt_id: \"smoke_prompt_1\",\n variant_tag: \"点击强化\",\n prompt_text: promptText,\n generation: { source: \"smoke-test-ui\", mode: \"image_to_image_with_prompt\" },\n },\n ],\n };\n io.generate = { input: generateInput };\n const generatedImages = await generateIcons(generateInput);\n io.generate.output = generatedImages;\n mark(\"S5 Icon 生成\", generatedImages.length ? \"passed\" : \"failed\", {\n image_count: generatedImages.length,\n first_image_url: generatedImages[0]?.url || \"\",\n first_scene_url: generatedImages[0]?.scene_url || \"\",\n });\n\n const exportInput = {\n ...payload,\n generatedImages,\n selectedImageIds: [generatedImages[0].image_id],\n sizes: [1024, 512, 256, 128, 64],\n };\n io.export = { input: exportInput };\n const exportResult = await exportIcons(exportInput);\n io.export.output = exportResult;\n mark(\"S8 多尺寸导出\", exportResult?.zip_url ? \"passed\" : \"failed\", {\n zip_url: exportResult?.zip_url || \"\",\n file_count: exportResult?.files?.length || 0,\n });\n }\n\n return {\n ok: steps.every((step) => step.status !== \"failed\"),\n mode: normalizedMode,\n started_at: startedAt,\n finished_at: new Date().toISOString(),\n base_url: publicBaseUrl(req),\n steps,\n io,\n };\n}\n\nfunction buildIconPrompt(input, index) {\n const workflow = buildWorkflowPayload(input);\n const direction = workflow.directions[index % workflow.directions.length] || \"点击强化\";\n const textRule = workflow.text_spec.enabled\n ? `Include a tiny bottom-right badge with the exact text \"${workflow.text_spec.text}\", max 5 characters.`\n : \"No text, no letters, no numbers, no watermark.\";\n\n return [\n \"Create a production-ready mobile app icon for paid app install ads.\",\n `Product: ${workflow.product}. Competitors: ${workflow.competitors}.`,\n `Platform: ${workflow.platform}. Creative direction: ${direction}.`,\n `Emotion target: ${workflow.emotion_target.join(\", \") || \"curiosity and high click appeal\"}.`,\n `Must emphasize: ${workflow.emphasis || \"clear core product identity and strong small-size recognizability\"}.`,\n `Must avoid: ${workflow.sensitive || \"misleading claims, gore, nudity, copyrighted logos, clutter\"}.`,\n `Reference notes: ${workflow.reference_notes || \"use uploaded references only as style inspiration, do not copy exact protected icons\"}.`,\n \"Icon constraints: one clear central subject, square 1:1 composition, transparent background with alpha channel, high contrast silhouette, readable at 64px, polished app-store quality, no border frame.\",\n \"Style: premium 3D-rendered app icon, vivid lighting, tactile materials, clean shape language, memorable focal point.\",\n textRule,\n \"Output only the icon artwork, transparent-background PNG.\",\n ].join(\"\\n\");\n}\n\nfunction getPromptJsonList(input) {\n const promptJson = Array.isArray(input.promptJson) ? input.promptJson : [];\n if (promptJson.length) return promptJson;\n\n const count = Math.max(1, Math.min(2, Number(input.count || 2)));\n return Array.from({ length: count }).map((_, index) => ({\n prompt_id: `server_fallback_prompt_${index + 1}`,\n variant_tag: (input.directions || [])[index % Math.max(1, (input.directions || []).length)] || \"点击强化\",\n prompt_text: buildIconPrompt(input, index),\n generation: { source: \"server fallback prompt_json\" },\n }));\n}\n\nfunction promptTextFromPromptJson(promptJson, input, index) {\n if (promptJson?.prompt_text) return promptJson.prompt_text;\n if (promptJson?.prompt) return promptJson.prompt;\n\n return [\n `Create a production-ready mobile app icon from S4 prompt_json ${promptJson?.prompt_id || index + 1}.`,\n `Variant: ${promptJson?.variant_tag || \"点击强化\"}.`,\n `Subject: ${promptJson?.subject || input.product || \"未填写\"}.`,\n `Style: ${JSON.stringify(promptJson?.style || \"premium app icon\")}.`,\n `Composition: ${JSON.stringify(promptJson?.composition || \"centered single subject\")}.`,\n `Emotion: ${JSON.stringify(promptJson?.emotion || input.emotion || [])}.`,\n `Constraints: ${JSON.stringify(promptJson?.constraints || {})}.`,\n promptJson?.text_spec?.enabled ? `Text badge: ${promptJson.text_spec.text}, bottom-right.` : \"No text, no letters, no numbers, no watermark.\",\n \"Icon constraints: square 1:1, transparent background with alpha channel, high contrast, readable at 64px, output only icon artwork.\",\n ].join(\"\\n\");\n}\n\nasync function generateIcons(input) {\n const promptJsonList = getPromptJsonList(input);\n const count = Math.max(1, Math.min(2, Number(input.count || promptJsonList.length || 2)));\n const images = [];\n const referencePack = await generationReferenceInputItems(input);\n const referenceImages = referencePack.images;\n const generationMode = referenceImages.length ? \"image_to_image_with_prompt\" : \"text_to_image\";\n\n for (let index = 0; index < count; index += 1) {\n const promptJson = promptJsonList[index] || promptJsonList[promptJsonList.length - 1];\n const prompt = promptTextFromPromptJson(promptJson, input, index);\n const image = await generateImage(prompt, referenceImages);\n\n const id = `icon_${Date.now()}_${index + 1}_${crypto.randomBytes(3).toString(\"hex\")}`;\n const filename = `${id}.png`;\n const iconBuffer = await makeIconTransparentPng(await resolveImageBuffer(image));\n fs.writeFileSync(path.join(outputDir, filename), iconBuffer);\n const sceneFilename = `${id}_scene.svg`;\n fs.writeFileSync(\n path.join(outputDir, sceneFilename),\n buildSceneTemplateSvg({\n iconBuffer,\n productName: input.googlePlayProfile?.app_title || input.product || \"游戏名称\",\n companyName: input.googlePlayProfile?.developer || \"Your Company\",\n platform: input.platform || \"Google Ads\",\n variant: promptJson?.variant_tag || (input.directions || [])[index % (input.directions || []).length] || \"点击强化\",\n }),\n );\n images.push({\n image_id: id,\n scene_image_id: `${id}_scene`,\n prompt_id: promptJson?.prompt_id || `server_fallback_prompt_${index + 1}`,\n variant_tag: promptJson?.variant_tag || (input.directions || [])[index % (input.directions || []).length] || \"点击强化\",\n url: `/generated/${filename}`,\n scene_url: `/generated/${sceneFilename}`,\n prompt_summary: prompt.split(\"\\n\").slice(0, 5).join(\" \"),\n prompt_source: promptJson?.generation?.source || \"S4 prompt_json\",\n reference_image_count: referenceImages.length,\n reference_image_sources: referencePack.sources,\n reference_image_errors: referencePack.errors,\n generation_mode: generationMode,\n scene_generation_mode: \"scene_template_composite\",\n });\n }\n\n return images;\n}\n\nasync function regenerateIcon(input) {\n const image = input.image || {};\n const currentCount = Number(image.regenerate_count || 0);\n if (currentCount >= 2) {\n throw new Error(\"该 icon 已达到最多 2 次重生限制。请回到 S3 修改提示词模板或最终提示词后重新生成。\");\n }\n\n const promptJson = input.promptJson || {};\n const prompt = String(input.promptText || promptJson.prompt_text || image.prompt_text || \"\").trim();\n if (!prompt) throw new Error(\"缺少用于重生的 prompt。\");\n\n const referencePack = await generationReferenceInputItems(input);\n const referenceImages = referencePack.images;\n const generationMode = referenceImages.length ? \"image_to_image_with_prompt\" : \"text_to_image\";\n const result = await generateImage(prompt, referenceImages);\n const iconBuffer = await makeIconTransparentPng(await resolveImageBuffer(result));\n\n const id = `icon_${Date.now()}_regen_${crypto.randomBytes(3).toString(\"hex\")}`;\n const filename = `${id}.png`;\n fs.writeFileSync(path.join(outputDir, filename), iconBuffer);\n\n const sceneFilename = `${id}_scene.svg`;\n fs.writeFileSync(\n path.join(outputDir, sceneFilename),\n buildSceneTemplateSvg({\n iconBuffer,\n productName: input.googlePlayProfile?.app_title || input.product || \"游戏名称\",\n companyName: input.googlePlayProfile?.developer || \"Your Company\",\n platform: input.platform || \"Google Ads\",\n variant: image.variant_tag || promptJson.variant_tag || \"重生\",\n }),\n );\n\n return {\n image_id: id,\n scene_image_id: `${id}_scene`,\n parent_image_id: image.image_id || \"\",\n version: Number(image.version || 1) + 1,\n regenerate_count: currentCount + 1,\n prompt_id: image.prompt_id || promptJson.prompt_id || \"regenerate_prompt\",\n prompt_text: prompt,\n variant_tag: image.variant_tag || promptJson.variant_tag || \"重生\",\n url: `/generated/${filename}`,\n scene_url: `/generated/${sceneFilename}`,\n prompt_summary: prompt.split(\"\\n\").slice(0, 5).join(\" \"),\n prompt_source: \"S7 edited original prompt\",\n reference_image_count: referenceImages.length,\n reference_image_sources: referencePack.sources,\n reference_image_errors: referencePack.errors,\n generation_mode: generationMode,\n scene_generation_mode: \"scene_template_composite\",\n };\n}\n\nasync function exportIcons(input) {\n const selectedIds = Array.isArray(input.selectedImageIds) ? input.selectedImageIds : [];\n const images = (input.generatedImages || []).filter((image) => selectedIds.includes(image.image_id));\n if (!images.length) throw new Error(\"请先在 S6 选择至少 1 个 icon。\");\n\n const sizes = (Array.isArray(input.sizes) && input.sizes.length ? input.sizes : [1024, 512, 256, 128, 64])\n .map((size) => Number(size))\n .filter((size) => [1024, 512, 256, 128, 64].includes(size));\n const productName = sanitizeFileName(input.product || input.googlePlayProfile?.app_title || \"product\");\n const packageId = `${productName}_icon_delivery_${Date.now()}`;\n const packageDir = path.join(exportDir, packageId);\n fs.mkdirSync(packageDir, { recursive: true });\n\n const files = [];\n for (const [imageIndex, image] of images.entries()) {\n const sourcePath = generatedPathFromUrl(image.url);\n if (!sourcePath || !fs.existsSync(sourcePath)) throw new Error(`找不到已生成 icon 文件:${image.image_id}`);\n const version = image.version || 1;\n const suffix = images.length > 1 ? `_choice${imageIndex + 1}` : \"\";\n for (const size of sizes) {\n const filename = `${productName}_icon${suffix}_v${version}_${size}px.png`;\n const outputPath = path.join(packageDir, filename);\n await sharp(sourcePath)\n .resize(size, size, { fit: \"cover\" })\n .png()\n .toFile(outputPath);\n files.push({\n image_id: image.image_id,\n size,\n filename,\n url: `/exports/${packageId}/${filename}`,\n });\n }\n\n }\n\n const zipName = `${packageId}.zip`;\n const zipPath = path.join(exportDir, zipName);\n await zipDirectory(packageDir, zipPath);\n\n return {\n package_id: packageId,\n zip_url: `/exports/${zipName}`,\n files,\n };\n}\n\nfunction generatedPathFromUrl(value = \"\") {\n if (!value || !value.startsWith(\"/generated/\")) return \"\";\n const filename = path.basename(value);\n return path.join(outputDir, filename);\n}\n\nfunction sanitizeFileName(value = \"\") {\n return String(value || \"product\")\n .trim()\n .replace(/\\s+/g, \"_\")\n .replace(/[^\\w\\u4e00-\\u9fa5-]/g, \"\")\n .toLowerCase() || \"product\";\n}\n\nfunction zipDirectory(sourceDir, zipPath) {\n const zip = new AdmZip();\n zip.addLocalFolder(sourceDir);\n zip.writeZip(zipPath);\n}\n\nasync function makeIconTransparentPng(buffer) {\n const image = sharp(buffer).ensureAlpha();\n const metadata = await image.metadata();\n const width = metadata.width || 0;\n const height = metadata.height || 0;\n if (!width || !height) return image.png().toBuffer();\n\n const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });\n const cornerSize = Math.max(4, Math.min(32, Math.floor(Math.min(info.width, info.height) * 0.035)));\n const samples = [];\n const addSample = (x, y) => {\n const offset = (y * info.width + x) * 4;\n samples.push([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]);\n };\n\n for (let y = 0; y < cornerSize; y += 1) {\n for (let x = 0; x < cornerSize; x += 1) {\n addSample(x, y);\n addSample(info.width - 1 - x, y);\n addSample(x, info.height - 1 - y);\n addSample(info.width - 1 - x, info.height - 1 - y);\n }\n }\n\n const avg = samples.reduce(\n (acc, sample) => {\n acc[0] += sample[0];\n acc[1] += sample[1];\n acc[2] += sample[2];\n acc[3] += sample[3];\n return acc;\n },\n [0, 0, 0, 0],\n ).map((value) => value / samples.length);\n\n if (avg[3] < 245) {\n return sharp(data, { raw: info }).png().toBuffer();\n }\n\n const threshold = 58;\n const visited = new Uint8Array(info.width * info.height);\n const queue = [];\n const isBackgroundLike = (x, y) => {\n const offset = (y * info.width + x) * 4;\n const distance = Math.hypot(data[offset] - avg[0], data[offset + 1] - avg[1], data[offset + 2] - avg[2]);\n return data[offset + 3] > 0 && distance <= threshold;\n };\n const enqueue = (x, y) => {\n if (x < 0 || y < 0 || x >= info.width || y >= info.height) return;\n const key = y * info.width + x;\n if (visited[key] || !isBackgroundLike(x, y)) return;\n visited[key] = 1;\n queue.push([x, y]);\n };\n\n for (let x = 0; x < info.width; x += 1) {\n enqueue(x, 0);\n enqueue(x, info.height - 1);\n }\n for (let y = 0; y < info.height; y += 1) {\n enqueue(0, y);\n enqueue(info.width - 1, y);\n }\n\n for (let index = 0; index < queue.length; index += 1) {\n const [x, y] = queue[index];\n data[(y * info.width + x) * 4 + 3] = 0;\n enqueue(x + 1, y);\n enqueue(x - 1, y);\n enqueue(x, y + 1);\n enqueue(x, y - 1);\n }\n\n return sharp(data, { raw: info }).png().toBuffer();\n}\n\nfunction providerHasKey(provider) {\n if (provider === \"volcengine\") return Boolean(arkApiKey);\n if (provider === \"aliyun\") return Boolean(dashscopeApiKey);\n if (provider === \"kimi\") return Boolean(kimiApiKey);\n if (provider === \"deepseek\") return Boolean(deepseekApiKey);\n return Boolean(apiKey);\n}\n\nasync function generateImage(prompt, referenceImages = []) {\n if (imageProvider === \"volcengine\") {\n const payload = {\n model: volcengineImageModel,\n prompt,\n size: volcengineImageSize,\n response_format: \"url\",\n };\n if (referenceImages.length) {\n payload.image = referenceImages[0];\n payload.images = referenceImages;\n payload.reference_images = referenceImages;\n payload.image_urls = referenceImages;\n }\n const result = await arkFetch(\"images/generations\", payload);\n return extractGeneratedImage(result);\n }\n\n if (imageProvider === \"aliyun\") {\n const task = await dashscopeFetch(\n \"services/aigc/image-generation/generation\",\n {\n model: aliyunImageModel,\n input: {\n messages: [\n {\n role: \"user\",\n content: [{ text: prompt }],\n },\n ],\n },\n parameters: {\n size: aliyunImageSize,\n n: 1,\n },\n },\n { async: true },\n );\n\n const taskId = task.output?.task_id;\n if (!taskId) {\n throw new Error(`DashScope image task did not return task_id: ${JSON.stringify(task)}`);\n }\n\n const result = await pollDashScopeTask(taskId);\n return extractGeneratedImage(result);\n }\n\n if (imageProvider === \"kimi\") {\n const result = await kimiFetchAbsolute(kimiImageEndpoint, {\n model: kimiImageModel,\n prompt,\n size: \"1024x1024\",\n quality: \"medium\",\n response_format: \"b64_json\",\n output_format: \"png\",\n });\n return extractGeneratedImage(result);\n }\n\n const result = await openaiFetch(\"images/generations\", {\n model: imageModel,\n prompt,\n size: \"1024x1024\",\n quality: \"medium\",\n output_format: \"png\",\n });\n return extractGeneratedImage(result);\n}\n\nfunction buildSceneTemplateSvg({ iconBuffer, productName, companyName, platform, variant }) {\n const iconData = `data:image/png;base64,${iconBuffer.toString(\"base64\")}`;\n const title = escapeXml(productName || \"游戏名称\");\n const company = escapeXml(companyName || \"Your Company\");\n const platformName = escapeXml(platform || \"Google Ads\");\n const variantName = escapeXml(variant || \"点击强化\");\n return Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1600\" height=\"1000\" viewBox=\"0 0 1600 1000\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#071320\"/>\n <stop offset=\"58%\" stop-color=\"#101b28\"/>\n <stop offset=\"100%\" stop-color=\"#050910\"/>\n </linearGradient>\n <linearGradient id=\"panel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#1d2a38\"/>\n <stop offset=\"100%\" stop-color=\"#0d141e\"/>\n </linearGradient>\n <filter id=\"shadow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\">\n <feDropShadow dx=\"0\" dy=\"18\" stdDeviation=\"16\" flood-color=\"#000\" flood-opacity=\".38\"/>\n </filter>\n <clipPath id=\"r40\"><rect width=\"220\" height=\"220\" rx=\"44\"/></clipPath>\n <clipPath id=\"r24\"><rect width=\"96\" height=\"96\" rx=\"22\"/></clipPath>\n <style>\n .title{font:800 38px Arial,\"Microsoft YaHei\",sans-serif;fill:#fff}\n .sub{font:400 20px Arial,\"Microsoft YaHei\",sans-serif;fill:#d7e3f2}\n .panelTitle{font:800 25px Arial,\"Microsoft YaHei\",sans-serif;fill:#fff}\n .small{font:600 15px Arial,\"Microsoft YaHei\",sans-serif;fill:#d8e1ec}\n .tiny{font:500 13px Arial,\"Microsoft YaHei\",sans-serif;fill:#aeb9c7}\n .darkCard{fill:url(#panel);stroke:#34465a;stroke-width:2}\n .whiteCard{fill:#fff}\n .muted{fill:#f5f7fb}\n .green{fill:#0aa36d}\n </style>\n </defs>\n <rect width=\"1600\" height=\"1000\" fill=\"url(#bg)\"/>\n <text x=\"800\" y=\"48\" text-anchor=\"middle\" class=\"title\">ICON 使用场景展示图模板(通用版)</text>\n <text x=\"800\" y=\"82\" text-anchor=\"middle\" class=\"sub\">适用于 ${platformName} / Meta / TikTok 等主流投放平台 · ${variantName}</text>\n\n <rect x=\"20\" y=\"110\" width=\"450\" height=\"850\" rx=\"18\" class=\"darkCard\"/>\n <text x=\"245\" y=\"145\" text-anchor=\"middle\" class=\"panelTitle\">1. 手机桌面效果</text>\n <text x=\"245\" y=\"172\" text-anchor=\"middle\" class=\"small\">展示 Icon 在手机桌面上的实际显示效果</text>\n <rect x=\"48\" y=\"200\" width=\"250\" height=\"620\" rx=\"34\" fill=\"#0a1220\" stroke=\"#46566a\" stroke-width=\"3\" filter=\"url(#shadow)\"/>\n <text x=\"78\" y=\"235\" class=\"small\">9:41</text>\n ${phoneIconGrid(iconData)}\n <rect x=\"78\" y=\"735\" width=\"190\" height=\"64\" rx=\"24\" fill=\"#223247\" opacity=\".86\"/>\n ${dockIcon(96, 753, \"#39d353\")}\n ${dockIcon(142, 753, \"#37a0ff\")}\n ${dockIcon(188, 753, \"#49d16d\")}\n ${dockIcon(234, 753, \"#ff4d55\")}\n <text x=\"50\" y=\"875\" class=\"small\">展示要点:</text>\n <text x=\"50\" y=\"905\" class=\"tiny\">• 不同系统桌面下的清晰度与吸引力</text>\n <text x=\"50\" y=\"930\" class=\"tiny\">• 小尺寸下的主体轮廓和识别度</text>\n\n <rect x=\"485\" y=\"110\" width=\"470\" height=\"850\" rx=\"18\" class=\"darkCard\"/>\n <text x=\"720\" y=\"145\" text-anchor=\"middle\" class=\"panelTitle\">2. Google Play 详情页效果</text>\n <text x=\"720\" y=\"172\" text-anchor=\"middle\" class=\"small\">展示 Icon 在商店详情页中的呈现效果</text>\n <rect x=\"510\" y=\"200\" width=\"420\" height=\"620\" rx=\"16\" class=\"whiteCard\" filter=\"url(#shadow)\"/>\n <text x=\"555\" y=\"242\" font-family=\"Arial\" font-size=\"24\" fill=\"#303846\">Google Play</text>\n <image href=\"${iconData}\" x=\"548\" y=\"290\" width=\"112\" height=\"112\"/>\n <text x=\"690\" y=\"320\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"30\" font-weight=\"800\" fill=\"#101828\">${title}</text>\n <text x=\"690\" y=\"354\" font-family=\"Arial\" font-size=\"18\" font-weight=\"700\" fill=\"#0a8f61\">${company}</text>\n <text x=\"690\" y=\"388\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"18\" fill=\"#344054\">解谜 · 冒险 · 探索</text>\n <text x=\"555\" y=\"455\" font-family=\"Arial\" font-size=\"22\" fill=\"#101828\">4.6★</text>\n <text x=\"680\" y=\"455\" font-family=\"Arial\" font-size=\"22\" fill=\"#101828\">1000万+</text>\n <text x=\"820\" y=\"455\" font-family=\"Arial\" font-size=\"22\" fill=\"#101828\">12+</text>\n <rect x=\"548\" y=\"505\" width=\"340\" height=\"52\" rx=\"8\" fill=\"#0aa36d\"/>\n <text x=\"718\" y=\"539\" text-anchor=\"middle\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"20\" font-weight=\"800\" fill=\"#fff\">安装</text>\n ${storeShotRow(iconData)}\n <text x=\"530\" y=\"875\" class=\"small\">展示要点:</text>\n <text x=\"530\" y=\"905\" class=\"tiny\">• 商店详情页中的清晰度与吸引力</text>\n <text x=\"530\" y=\"930\" class=\"tiny\">• 与应用名称、分类、评分的搭配效果</text>\n\n <rect x=\"970\" y=\"110\" width=\"610\" height=\"850\" rx=\"18\" class=\"darkCard\"/>\n <text x=\"1275\" y=\"145\" text-anchor=\"middle\" class=\"panelTitle\">3. 广告平台主要广告展示效果</text>\n <text x=\"1275\" y=\"172\" text-anchor=\"middle\" class=\"small\">展示 Icon 在不同广告位中的实际呈现效果</text>\n ${adCards(iconData, title, company)}\n <text x=\"995\" y=\"875\" class=\"small\">展示要点:</text>\n <text x=\"995\" y=\"905\" class=\"tiny\">• 不同广告位和尺寸下的表现</text>\n <text x=\"995\" y=\"930\" class=\"tiny\">• 与文案、素材的搭配协调性</text>\n <text x=\"1240\" y=\"905\" class=\"tiny\">• 吸引用户点击的视觉表现</text>\n <text x=\"800\" y=\"984\" text-anchor=\"middle\" class=\"tiny\">注:场景展示图由生成 Icon 原样嵌入模板,仅用于投放预览。</text>\n</svg>`);\n}\n\nfunction phoneIconGrid(iconData) {\n const colors = [\"#ffffff\", \"#f2b84b\", \"#5aa7ff\", \"#d7dde7\", \"#46c970\", \"#3182ff\", \"#fff\", \"#a7b2c3\"];\n const cells = [];\n for (let row = 0; row < 4; row += 1) {\n for (let col = 0; col < 4; col += 1) {\n const x = 75 + col * 52;\n const y = 275 + row * 84;\n if (row === 2 && col === 0) {\n cells.push(`<image href=\"${iconData}\" x=\"${x}\" y=\"${y}\" width=\"42\" height=\"42\"/><text x=\"${x + 21}\" y=\"${y + 62}\" text-anchor=\"middle\" class=\"tiny\">游戏</text>`);\n } else {\n cells.push(`<rect x=\"${x}\" y=\"${y}\" width=\"42\" height=\"42\" rx=\"10\" fill=\"${colors[(row * 4 + col) % colors.length]}\"/><text x=\"${x + 21}\" y=\"${y + 62}\" text-anchor=\"middle\" class=\"tiny\">App</text>`);\n }\n }\n }\n return cells.join(\"\");\n}\n\nfunction dockIcon(x, y, color) {\n return `<rect x=\"${x}\" y=\"${y}\" width=\"34\" height=\"34\" rx=\"9\" fill=\"${color}\"/>`;\n}\n\nfunction storeShotRow(iconData) {\n return [0, 1, 2, 3]\n .map((i) => `<rect x=\"${548 + i * 86}\" y=\"600\" width=\"72\" height=\"150\" rx=\"10\" fill=\"#111827\"/><image href=\"${iconData}\" x=\"${560 + i * 86}\" y=\"635\" width=\"48\" height=\"48\"/>`)\n .join(\"\");\n}\n\nfunction adCards(iconData, title, company) {\n return `\n <rect x=\"995\" y=\"210\" width=\"300\" height=\"160\" rx=\"12\" fill=\"#f8fafc\"/>\n <image href=\"${iconData}\" x=\"1020\" y=\"245\" width=\"64\" height=\"64\"/>\n <text x=\"1100\" y=\"260\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"20\" font-weight=\"800\" fill=\"#111827\">${title}</text>\n <text x=\"1100\" y=\"286\" font-family=\"Arial\" font-size=\"14\" fill=\"#475467\">${company}</text>\n <text x=\"1100\" y=\"318\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"16\" fill=\"#111827\">探索神秘旅程!</text>\n <rect x=\"1218\" y=\"320\" width=\"54\" height=\"28\" rx=\"7\" fill=\"#2f6bff\"/><text x=\"1245\" y=\"339\" text-anchor=\"middle\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"14\" font-weight=\"800\" fill=\"#fff\">安装</text>\n\n <rect x=\"995\" y=\"395\" width=\"300\" height=\"245\" rx=\"12\" fill=\"#f8fafc\"/>\n <image href=\"${iconData}\" x=\"1020\" y=\"425\" width=\"42\" height=\"42\"/>\n <text x=\"1075\" y=\"442\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"18\" font-weight=\"800\" fill=\"#111827\">${title}</text>\n <rect x=\"1020\" y=\"480\" width=\"250\" height=\"110\" rx=\"10\" fill=\"#0d1724\"/>\n <image href=\"${iconData}\" x=\"1110\" y=\"500\" width=\"70\" height=\"70\"/>\n <text x=\"1020\" y=\"622\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"14\" fill=\"#111827\">立即下载,开启冒险!</text>\n\n <rect x=\"1320\" y=\"210\" width=\"210\" height=\"430\" rx=\"18\" fill=\"#080e17\"/>\n <rect x=\"1345\" y=\"240\" width=\"160\" height=\"260\" rx=\"10\" fill=\"#122033\"/>\n <image href=\"${iconData}\" x=\"1378\" y=\"310\" width=\"96\" height=\"96\"/>\n <text x=\"1360\" y=\"535\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"18\" font-weight=\"800\" fill=\"#fff\">${title}</text>\n <text x=\"1360\" y=\"562\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"14\" fill=\"#d7e3f2\">探索隐藏奖励!</text>\n <rect x=\"1360\" y=\"590\" width=\"120\" height=\"34\" rx=\"7\" fill=\"#ff2f55\"/><text x=\"1420\" y=\"613\" text-anchor=\"middle\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"15\" font-weight=\"800\" fill=\"#fff\">立即下载</text>`;\n}\n\nfunction escapeXml(value = \"\") {\n return String(value)\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nasync function pollDashScopeTask(taskId) {\n const deadline = Date.now() + 1000 * 60 * 6;\n let lastResult = null;\n\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 2500));\n lastResult = await dashscopeGet(`tasks/${taskId}`);\n const status = lastResult.output?.task_status || lastResult.output?.status;\n if (status === \"SUCCEEDED\") return lastResult;\n if (status === \"FAILED\" || status === \"UNKNOWN\") {\n throw new Error(lastResult.output?.message || `DashScope image task ${status}`);\n }\n }\n\n throw new Error(`DashScope image task timed out: ${taskId}. Last result: ${JSON.stringify(lastResult)}`);\n}\n\nfunction extractGeneratedImage(result) {\n const image =\n result.data?.[0] ||\n result.images?.[0] ||\n result.output?.[0] ||\n result.output?.results?.[0] ||\n result.output?.task_results?.[0] ||\n result.output?.choices?.[0]?.message?.content?.find((part) => part.image || part.image_url || part.url);\n if (image?.b64_json || image?.url || image?.image || image?.image_url) return image;\n\n const content = result.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const item = content.find((part) => part.image_url?.url || part.url || part.b64_json);\n if (item) return item.image_url || item;\n }\n\n const url = result.output?.url || result.output?.image_url || result.url || result.image_url || result.image;\n if (url) return { url: typeof url === \"string\" ? url : url.url };\n\n throw new Error(\"Image API did not return b64_json, url, or image field. Check image provider response format.\");\n}\n\nasync function resolveImageBuffer(image) {\n if (image.b64_json) {\n return Buffer.from(image.b64_json, \"base64\");\n }\n\n const url = image.url || image.image || image.image_url?.url || image.image_url;\n if (!url) {\n throw new Error(\"Image API result has no b64_json or url.\");\n }\n\n if (url.startsWith(\"data:image/\")) {\n return Buffer.from(url.split(\",\")[1] || \"\", \"base64\");\n }\n\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to download generated image: HTTP ${response.status}`);\n }\n return Buffer.from(await response.arrayBuffer());\n}\n\nasync function qaImages(input) {\n const images = input.generatedImages || [];\n const reports = [];\n\n for (const image of images) {\n const filename = path.basename(image.url || \"\");\n const filePath = path.join(outputDir, filename);\n if (!fs.existsSync(filePath)) continue;\n\n const b64 = fs.readFileSync(filePath).toString(\"base64\");\n const text = await runStructuredAnalysis({\n text: `请作为广告投放 icon 质检员,对这张 icon 做自动质检,只返回 JSON。\n检查项:\n- must_include 是否体现产品主体:${input.product || \"未填写\"}\n- must_not_include 是否避开:${input.sensitive || \"误导承诺、血腥、裸露、侵权元素\"}\n- 64px 小尺寸识别度\n- 单主体、中心构图、高对比、简单背景\n- 平台审核风险:${input.platform || \"Google Ads\"}\n输出字段:image_id, passed, score_0_100, issues[], recommendation。`,\n images: [\n {\n type: \"input_image\",\n image_url: `data:image/png;base64,${b64}`,\n detail: \"low\",\n },\n ],\n });\n\n const parsed = parseJsonLoose(text);\n reports.push(parsed ? { image_id: image.image_id, ...parsed } : { image_id: image.image_id, passed: false, raw_text: text });\n }\n\n return reports;\n}\n\nfunction serveStatic(req, res) {\n const url = new URL(req.url, `http://localhost:${port}`);\n const pathname = decodeURIComponent(url.pathname);\n const relativePath = pathname === \"/\" ? \"index.html\" : pathname.replace(/^\\/+/, \"\");\n const filePath = path.normalize(path.join(rootDir, relativePath));\n\n if (!filePath.startsWith(rootDir)) {\n res.writeHead(403, corsHeaders());\n res.end(\"Forbidden\");\n return;\n }\n\n fs.readFile(filePath, (error, data) => {\n if (error) {\n res.writeHead(404, corsHeaders());\n res.end(\"Not found\");\n return;\n }\n\n const ext = path.extname(filePath).toLowerCase();\n res.writeHead(200, {\n ...corsHeaders(),\n \"Content-Type\": mimeTypes[ext] || \"application/octet-stream\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(data);\n });\n}\n\nconst server = http.createServer(async (req, res) => {\n try {\n const url = new URL(req.url, `http://localhost:${port}`);\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders());\n res.end();\n return;\n }\n\n if (url.pathname === \"/api/health\") {\n sendJson(res, 200, {\n ok: true,\n ai_provider: aiProvider,\n image_provider: imageProvider,\n has_api_key: providerHasKey(aiProvider) && providerHasKey(imageProvider),\n has_kimi_api_key: Boolean(kimiApiKey),\n has_deepseek_api_key: Boolean(deepseekApiKey),\n has_dashscope_api_key: Boolean(dashscopeApiKey),\n has_ark_api_key: Boolean(arkApiKey),\n text_model: textModel,\n kimi_text_model: kimiTextModel,\n deepseek_text_model: deepseekTextModel,\n aliyun_text_model: aliyunTextModel,\n volcengine_text_model: volcengineTextModel,\n image_model: imageModel,\n kimi_image_model: kimiImageModel,\n kimi_image_endpoint: kimiImageEndpoint,\n aliyun_image_model: aliyunImageModel,\n aliyun_image_size: aliyunImageSize,\n volcengine_image_model: volcengineImageModel,\n volcengine_image_size: volcengineImageSize,\n ark_base_url: arkBaseUrl,\n });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/analyze\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, analysis: await analyzeWorkflow(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/optimize-prompts\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, prompt_plan: await optimizePromptsConfigured(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/check-prompts\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, report: await checkPrompts(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/google-play\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, google_play: await googlePlayLookup(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/upload-reference\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, file: await uploadReferenceImage(body, req) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/generate-icons\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, images: await generateIcons(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/regenerate-icon\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, image: await regenerateIcon(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/export-icons\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, export: await exportIcons(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/qa\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, reports: await qaImages(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/smoke-test\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, result: await runSmokeTest(String(body.mode || \"quick\"), req) });\n return;\n }\n\n serveStatic(req, res);\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error.message });\n }\n});\n\nserver.listen(port, () => {\n console.log(`应用icon创作大师已启动:http://localhost:${port}`);\n console.log(`模型状态:${apiKey ? \"已读取 OPENAI_API_KEY\" : \"未设置 OPENAI_API_KEY\"}`);\n});\n"}, "6ff4f7e142d7c657c9db1ccd6a6502afeac527a9": {"t": 1785754026.228795, "n": 86906, "h": "f13115539aeadc7d", "c": "const http = require(\"node:http\");\nconst fs = require(\"node:fs\");\nconst path = require(\"node:path\");\nconst crypto = require(\"node:crypto\");\nconst os = require(\"node:os\");\nconst { execFile } = require(\"node:child_process\");\nconst sharp = require(\"sharp\");\nconst AdmZip = require(\"adm-zip\");\n\nconst rootDir = __dirname;\nconst appConfig = loadAppConfig();\nconst outputDir = path.join(rootDir, \"generated\");\nconst exportDir = path.join(rootDir, \"exports\");\nconst uploadDir = path.join(rootDir, \"uploads\");\nconst port = Number(process.env.PORT || 8787);\n\nloadLocalEnv();\n\nconst apiKey = process.env.OPENAI_API_KEY || \"\";\nconst aiProvider = (process.env.AI_PROVIDER || appConfig.models?.text?.provider || \"openai\").toLowerCase();\nconst imageProvider = (process.env.IMAGE_PROVIDER || appConfig.models?.image?.provider || \"openai\").toLowerCase();\nconst kimiApiKey = process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || \"\";\nconst kimiBaseUrl = (process.env.KIMI_BASE_URL || \"https://api.moonshot.cn/v1\").replace(/\\/+$/, \"\");\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || \"\";\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || appConfig.models?.text?.base_url || \"https://api.deepseek.com/v1\").replace(/\\/+$/, \"\");\nconst dashscopeApiKey = process.env.DASHSCOPE_API_KEY || process.env.ALIYUN_API_KEY || \"\";\nconst dashscopeBaseUrl = (process.env.DASHSCOPE_BASE_URL || \"https://dashscope.aliyuncs.com/api/v1\").replace(/\\/+$/, \"\");\nconst arkApiKey = process.env.ARK_API_KEY || process.env.VOLCENGINE_API_KEY || \"\";\nconst arkBaseUrl = (process.env.ARK_BASE_URL || \"https://ark.cn-beijing.volces.com/api/v3\").replace(/\\/+$/, \"\");\nconst textModel = process.env.OPENAI_TEXT_MODEL || \"gpt-4.1\";\nconst kimiTextModel = process.env.KIMI_TEXT_MODEL || \"kimi-k2.6\";\nconst deepseekTextModel = process.env.DEEPSEEK_TEXT_MODEL || appConfig.models?.text?.model || \"deepseek-v4-pro\";\nconst aliyunTextModel = process.env.ALIYUN_TEXT_MODEL || \"qwen-plus\";\nconst volcengineTextModel = process.env.VOLCENGINE_TEXT_MODEL || process.env.ARK_TEXT_MODEL || \"doubao-seed-1-6-251015\";\nconst imageModel = process.env.OPENAI_IMAGE_MODEL || \"gpt-image-1\";\nconst kimiImageModel = process.env.KIMI_IMAGE_MODEL || process.env.KIMI_TEXT_MODEL || \"kimi-k2.6\";\nconst kimiImageEndpoint = process.env.KIMI_IMAGE_ENDPOINT || `${kimiBaseUrl}/images/generations`;\nconst aliyunImageModel = process.env.ALIYUN_IMAGE_MODEL || \"wan2.7-image\";\nconst aliyunImageSize = process.env.ALIYUN_IMAGE_SIZE || \"1K\";\nconst volcengineImageModel = process.env.VOLCENGINE_IMAGE_MODEL || process.env.ARK_IMAGE_MODEL || appConfig.models?.image?.model || \"doubao-seedream-3-0-t2i-250415\";\nconst volcengineImageSize = process.env.VOLCENGINE_IMAGE_SIZE || appConfig.models?.image?.size || \"2048x2048\";\n\nfs.mkdirSync(outputDir, { recursive: true });\nfs.mkdirSync(exportDir, { recursive: true });\nfs.mkdirSync(uploadDir, { recursive: true });\n\nfunction loadLocalEnv() {\n const envPath = path.join(rootDir, \".env.local\");\n if (!fs.existsSync(envPath)) return;\n\n const lines = fs.readFileSync(envPath, \"utf8\").split(/\\r?\\n/);\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) continue;\n const key = trimmed.slice(0, eq).trim();\n const value = trimmed.slice(eq + 1).trim().replace(/^[\"']|[\"']$/g, \"\");\n if (key && process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n}\n\nfunction loadAppConfig() {\n const configPath = path.join(rootDir, \"icon-agent.config.json\");\n if (!fs.existsSync(configPath)) return {};\n try {\n return JSON.parse(fs.readFileSync(configPath, \"utf8\"));\n } catch (error) {\n console.warn(`Failed to read icon-agent.config.json: ${error.message}`);\n return {};\n }\n}\n\nconst mimeTypes = {\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"application/javascript; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".webp\": \"image/webp\",\n \".svg\": \"image/svg+xml\",\n \".zip\": \"application/zip\",\n};\n\nfunction sendJson(res, status, payload) {\n res.writeHead(status, {\n ...corsHeaders(),\n \"Content-Type\": \"application/json; charset=utf-8\",\n });\n res.end(JSON.stringify(payload));\n}\n\nfunction corsHeaders() {\n return {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Methods\": \"GET,POST,OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type,Authorization\",\n };\n}\n\nfunction readBody(req) {\n return new Promise((resolve, reject) => {\n let body = \"\";\n req.on(\"data\", (chunk) => {\n body += chunk;\n if (body.length > 30 * 1024 * 1024) {\n reject(new Error(\"Request body is too large.\"));\n req.destroy();\n }\n });\n req.on(\"end\", () => {\n try {\n resolve(body ? JSON.parse(body) : {});\n } catch (error) {\n reject(error);\n }\n });\n req.on(\"error\", reject);\n });\n}\n\nfunction parseDataUrl(dataUrl = \"\") {\n const match = String(dataUrl).match(/^data:([^;,]+);base64,(.+)$/s);\n if (!match) throw new Error(\"Invalid image data URL.\");\n return {\n mime: match[1].toLowerCase(),\n buffer: Buffer.from(match[2], \"base64\"),\n };\n}\n\nfunction renderTemplate(template = \"\", values = {}) {\n return String(template).replace(/\\{([A-Z0-9_]+)\\}/g, (_, key) => {\n const value = values[key];\n if (typeof value === \"string\") return value;\n return JSON.stringify(value ?? \"\", null, 2);\n });\n}\n\nfunction extensionFromMime(mime = \"\") {\n if (mime === \"image/jpeg\" || mime === \"image/jpg\") return \".jpg\";\n if (mime === \"image/png\") return \".png\";\n if (mime === \"image/webp\") return \".webp\";\n if (mime === \"image/gif\") return \".gif\";\n return \"\";\n}\n\nfunction publicBaseUrl(req) {\n const configured = (process.env.PUBLIC_BASE_URL || process.env.APP_PUBLIC_URL || \"\").replace(/\\/+$/, \"\");\n if (configured) return configured;\n const proto = req.headers[\"x-forwarded-proto\"] || (req.socket.encrypted ? \"https\" : \"http\");\n const host = req.headers[\"x-forwarded-host\"] || req.headers.host || `localhost:${port}`;\n return `${String(proto).split(\",\")[0]}://${String(host).split(\",\")[0]}`;\n}\n\nasync function uploadReferenceImage(input, req) {\n const { mime, buffer } = parseDataUrl(input.dataUrl || input.image || \"\");\n if (!mime.startsWith(\"image/\")) throw new Error(\"Only image uploads are supported.\");\n if (buffer.length > 8 * 1024 * 1024) throw new Error(\"Reference image must be 8MB or smaller.\");\n\n const ext = extensionFromMime(mime);\n if (!ext) throw new Error(`Unsupported image type: ${mime}`);\n\n const safeBase = sanitizeFileName(path.basename(input.name || \"reference\", path.extname(input.name || \"\"))) || \"reference\";\n const id = `${Date.now()}_${crypto.randomBytes(5).toString(\"hex\")}`;\n const filename = `${safeBase}_${id}${ext}`;\n const filePath = path.join(uploadDir, filename);\n fs.writeFileSync(filePath, buffer);\n\n const urlPath = `/uploads/${filename}`;\n return {\n id,\n name: input.name || filename,\n type: mime,\n size: buffer.length,\n url: urlPath,\n public_url: `${publicBaseUrl(req)}${urlPath}`,\n };\n}\n\nasync function openaiFetch(endpoint, payload) {\n if (!apiKey) {\n throw new Error(\"Missing OPENAI_API_KEY. Please set it before starting the server.\");\n }\n\n try {\n return await openaiFetchWithNode(endpoint, payload);\n } catch (error) {\n if (!/fetch failed|Connect Timeout|UND_ERR_CONNECT_TIMEOUT/i.test(error.message + \" \" + (error.cause?.code || \"\"))) {\n throw error;\n }\n console.warn(\"Node fetch failed; retrying OpenAI request through PowerShell HTTPS.\");\n return openaiFetchWithPowerShell(endpoint, payload);\n }\n}\n\nasync function kimiFetch(endpoint, payload) {\n if (!kimiApiKey) {\n throw new Error(\"Missing KIMI_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${kimiBaseUrl}/${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${kimiApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function deepseekFetch(endpoint, payload) {\n if (!deepseekApiKey) {\n throw new Error(\"Missing DEEPSEEK_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${deepseekBaseUrl}/${endpoint.replace(/^\\/+/, \"\")}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${deepseekApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function kimiFetchAbsolute(url, payload) {\n if (!kimiApiKey) {\n throw new Error(\"Missing KIMI_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${kimiApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function dashscopeFetch(pathname, payload, options = {}) {\n if (!dashscopeApiKey) {\n throw new Error(\"Missing DASHSCOPE_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${dashscopeBaseUrl}/${pathname.replace(/^\\/+/, \"\")}`, {\n method: options.method || \"POST\",\n headers: {\n Authorization: `Bearer ${dashscopeApiKey}`,\n \"Content-Type\": \"application/json\",\n ...(options.async ? { \"X-DashScope-Async\": \"enable\" } : {}),\n },\n body: payload ? JSON.stringify(payload) : undefined,\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function dashscopeGet(pathname) {\n if (!dashscopeApiKey) {\n throw new Error(\"Missing DASHSCOPE_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${dashscopeBaseUrl}/${pathname.replace(/^\\/+/, \"\")}`, {\n method: \"GET\",\n headers: { Authorization: `Bearer ${dashscopeApiKey}` },\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function arkFetch(pathname, payload) {\n if (!arkApiKey) {\n throw new Error(\"Missing ARK_API_KEY. Please set it in .env.local before starting the server.\");\n }\n\n const response = await fetch(`${arkBaseUrl}/${pathname.replace(/^\\/+/, \"\")}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${arkApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nasync function openaiFetchWithNode(endpoint, payload) {\n const response = await fetch(`https://api.openai.com/v1/${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n\n return parseOpenAIResponse(response.status, response.statusText, await response.text());\n}\n\nfunction parseOpenAIResponse(status, statusText, text) {\n let data;\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = { raw: text };\n }\n\n if (status < 200 || status >= 300) {\n const message = data?.error?.message || data?.raw || statusText || `HTTP ${status}`;\n throw new Error(message);\n }\n\n return data;\n}\n\nfunction psQuote(value) {\n return `'${String(value).replace(/'/g, \"''\")}'`;\n}\n\nfunction execFileAsync(file, args, options = {}) {\n return new Promise((resolve, reject) => {\n execFile(file, args, { maxBuffer: 1024 * 1024 * 80, ...options }, (error, stdout, stderr) => {\n if (error) {\n error.stdout = stdout;\n error.stderr = stderr;\n reject(error);\n return;\n }\n resolve({ stdout, stderr });\n });\n });\n}\n\nasync function openaiFetchWithPowerShell(endpoint, payload) {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"icon-agent-openai-\"));\n const bodyPath = path.join(tmpDir, \"request.json\");\n const responsePath = path.join(tmpDir, \"response.json\");\n const statusPath = path.join(tmpDir, \"status.txt\");\n fs.writeFileSync(bodyPath, JSON.stringify(payload), \"utf8\");\n\n const command = `\n$ErrorActionPreference = \"Stop\"\n$body = Get-Content -LiteralPath ${psQuote(bodyPath)} -Raw\ntry {\n Add-Type -AssemblyName System.Net.Http\n $client = [System.Net.Http.HttpClient]::new()\n $client.Timeout = [TimeSpan]::FromSeconds(300)\n $client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new(\"Bearer\", $env:OPENAI_API_KEY)\n $content = [System.Net.Http.StringContent]::new($body, [System.Text.Encoding]::UTF8, \"application/json\")\n $response = $client.PostAsync(${psQuote(`https://api.openai.com/v1/${endpoint}`)}, $content).Result\n $responseBody = $response.Content.ReadAsStringAsync().Result\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value ([int]$response.StatusCode) -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(responsePath)} -Value $responseBody -Encoding UTF8\n if (-not $response.IsSuccessStatusCode) { exit 1 }\n} catch {\n $status = 500\n $content = $_.Exception.Message\n if (Test-Path ${psQuote(statusPath)}) { $status = [int](Get-Content -LiteralPath ${psQuote(statusPath)} -Raw) }\n if (Test-Path ${psQuote(responsePath)}) { $content = Get-Content -LiteralPath ${psQuote(responsePath)} -Raw }\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value $status -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(responsePath)} -Value $content -Encoding UTF8\n exit 1\n}\n`;\n\n try {\n await execFileAsync(\"powershell\", [\"-NoProfile\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", command], {\n env: { ...process.env, OPENAI_API_KEY: apiKey },\n });\n } catch {\n // The PowerShell script writes the response body/status even for OpenAI 4xx/5xx errors.\n }\n\n const status = Number(fs.existsSync(statusPath) ? fs.readFileSync(statusPath, \"utf8\").trim() : \"500\");\n const text = fs.existsSync(responsePath) ? fs.readFileSync(responsePath, \"utf8\") : \"\";\n fs.rmSync(tmpDir, { recursive: true, force: true });\n return parseOpenAIResponse(status, `HTTP ${status}`, text);\n}\n\nasync function fetchTextWithFallback(url) {\n try {\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n },\n });\n if (response.ok) return response.text();\n throw new Error(`HTTP ${response.status}`);\n } catch {\n return fetchTextWithPowerShell(url);\n }\n}\n\nasync function fetchTextWithPowerShell(url) {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"icon-agent-web-\"));\n const outputPath = path.join(tmpDir, \"response.txt\");\n const statusPath = path.join(tmpDir, \"status.txt\");\n const command = `\n$ErrorActionPreference = \"Stop\"\ntry {\n $headers = @{ \"User-Agent\" = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"; \"Accept-Language\" = \"en-US,en;q=0.9\" }\n $response = Invoke-WebRequest -Uri ${psQuote(url)} -Headers $headers -TimeoutSec 60\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value ([int]$response.StatusCode) -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(outputPath)} -Value $response.Content -Encoding UTF8\n} catch {\n Set-Content -LiteralPath ${psQuote(statusPath)} -Value 500 -Encoding ASCII\n Set-Content -LiteralPath ${psQuote(outputPath)} -Value $_.Exception.Message -Encoding UTF8\n exit 1\n}\n`;\n\n try {\n await execFileAsync(\"powershell\", [\"-NoProfile\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", command], {\n maxBuffer: 1024 * 1024 * 30,\n });\n } catch {\n // Read the captured response below.\n }\n\n const status = Number(fs.existsSync(statusPath) ? fs.readFileSync(statusPath, \"utf8\").trim() : \"500\");\n const text = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, \"utf8\") : \"\";\n fs.rmSync(tmpDir, { recursive: true, force: true });\n if (status < 200 || status >= 300) throw new Error(`Fetch failed for ${url}: ${text || status}`);\n return text;\n}\n\nasync function fetchWithFallbackBinary(url) {\n try {\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\",\n \"Accept-Language\": \"en-US,en;q=0.9\",\n },\n });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return {\n buffer: Buffer.from(await response.arrayBuffer()),\n mime: response.headers.get(\"content-type\")?.split(\";\")[0] || \"image/png\",\n };\n } catch {\n return fetchBinaryWithPowerShell(url);\n }\n}\n\nasync function fetchBinaryWithPowerShell(url) {\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"icon-agent-img-\"));\n const outputPath = path.join(tmpDir, \"image.bin\");\n const metaPath = path.join(tmpDir, \"meta.json\");\n const command = `\n$ErrorActionPreference = \"Stop\"\ntry {\n Add-Type -AssemblyName System.Net.Http\n $client = [System.Net.Http.HttpClient]::new()\n $client.Timeout = [TimeSpan]::FromSeconds(90)\n $client.DefaultRequestHeaders.UserAgent.ParseAdd(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n $response = $client.GetAsync(${psQuote(url)}).Result\n $bytes = $response.Content.ReadAsByteArrayAsync().Result\n [System.IO.File]::WriteAllBytes(${psQuote(outputPath)}, $bytes)\n $mime = \"image/png\"\n if ($response.Content.Headers.ContentType) { $mime = $response.Content.Headers.ContentType.MediaType }\n @{ status = [int]$response.StatusCode; mime = $mime } | ConvertTo-Json | Set-Content -LiteralPath ${psQuote(metaPath)} -Encoding UTF8\n if (-not $response.IsSuccessStatusCode) { exit 1 }\n} catch {\n @{ status = 500; mime = \"image/png\"; error = $_.Exception.Message } | ConvertTo-Json | Set-Content -LiteralPath ${psQuote(metaPath)} -Encoding UTF8\n exit 1\n}\n`;\n\n try {\n await execFileAsync(\"powershell\", [\"-NoProfile\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\", command], {\n maxBuffer: 1024 * 1024 * 30,\n });\n } catch {\n // Inspect the meta file below.\n }\n\n const meta = fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath, \"utf8\").replace(/^\\uFEFF/, \"\")) : { status: 500 };\n if (!fs.existsSync(outputPath) || meta.status < 200 || meta.status >= 300) {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n throw new Error(meta.error || `Image download failed: HTTP ${meta.status}`);\n }\n const buffer = fs.readFileSync(outputPath);\n fs.rmSync(tmpDir, { recursive: true, force: true });\n return { buffer, mime: meta.mime || \"image/png\" };\n}\n\nfunction extractText(response) {\n if (typeof response.output_text === \"string\") return response.output_text;\n const chunks = [];\n for (const item of response.output || []) {\n for (const content of item.content || []) {\n if (content.type === \"output_text\" || content.type === \"text\") {\n chunks.push(content.text);\n }\n }\n }\n return chunks.join(\"\\n\").trim();\n}\n\nfunction parseJsonLoose(text) {\n try {\n return JSON.parse(text);\n } catch {\n const match = text.match(/\\{[\\s\\S]*\\}/);\n if (match) {\n try {\n return JSON.parse(match[0]);\n } catch {\n return null;\n }\n }\n return null;\n }\n}\n\nfunction htmlDecode(value = \"\") {\n return String(value)\n .replace(/\\\\u003d/g, \"=\")\n .replace(/\\\\u0026/g, \"&\")\n .replace(/\\\\u003c/g, \"<\")\n .replace(/\\\\u003e/g, \">\")\n .replace(/&amp;/g, \"&\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/\\\\n/g, \"\\n\");\n}\n\nfunction unique(values) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction imageInputItems(files = []) {\n return files\n .filter((file) => (file.publicUrl || file.dataUrl) && file.type?.startsWith(\"image/\"))\n .slice(0, 2)\n .map((file) => ({\n type: \"input_image\",\n image_url: file.dataUrl || localUploadDataUrlFromPublicUrl(file.publicUrl) || (String(file.publicUrl || \"\").includes(\"/uploads/\") ? \"\" : file.publicUrl),\n public_url: file.publicUrl || \"\",\n detail: \"low\",\n name: file.name || \"\",\n file_type: file.type || \"\",\n }))\n .filter((item) => item.image_url);\n}\n\nfunction mimeTypeFromFileName(filename = \"\") {\n const ext = path.extname(filename).toLowerCase();\n if (ext === \".jpg\" || ext === \".jpeg\") return \"image/jpeg\";\n if (ext === \".webp\") return \"image/webp\";\n if (ext === \".gif\") return \"image/gif\";\n if (ext === \".svg\") return \"image/svg+xml\";\n return \"image/png\";\n}\n\nfunction localUploadDataUrlFromPublicUrl(publicUrl = \"\") {\n if (!publicUrl) return \"\";\n let pathname = \"\";\n try {\n pathname = new URL(publicUrl, getPublicBaseUrl()).pathname;\n } catch {\n pathname = publicUrl;\n }\n if (!pathname.startsWith(\"/uploads/\")) return \"\";\n const filename = path.basename(decodeURIComponent(pathname));\n const resolved = path.resolve(uploadDir, filename);\n if (!resolved.startsWith(path.resolve(uploadDir) + path.sep) || !fs.existsSync(resolved)) return \"\";\n const buffer = fs.readFileSync(resolved);\n return `data:${mimeTypeFromFileName(filename)};base64,${buffer.toString(\"base64\")}`;\n}\n\nasync function visualReferenceInputItems(input) {\n const uploaded = imageInputItems(input.referenceFiles);\n const refs = getGooglePlayReferences(input);\n const competitorRefs = getCompetitorGooglePlayReferences(input);\n const googlePlayUrls = [\n refs.icon,\n refs.featureGraphic,\n ...(refs.screenshots || []).slice(0, 4),\n ...competitorRefs.map((item) => item.icon),\n ].filter(Boolean);\n\n if (aiProvider === \"deepseek\") {\n const googlePlay = googlePlayUrls.map((url) => ({\n type: \"input_image\",\n image_url: url,\n public_url: url,\n detail: \"low\",\n }));\n return [...uploaded, ...googlePlay].slice(0, 8);\n }\n\n const googlePlay = [];\n for (const url of googlePlayUrls) {\n try {\n googlePlay.push({\n type: \"input_image\",\n image_url: await imageUrlToDataUrl(url),\n public_url: url,\n detail: \"low\",\n });\n } catch (error) {\n console.warn(`Failed to inline Google Play image: ${url} ${error.message}`);\n }\n }\n return [...uploaded, ...googlePlay].slice(0, 8);\n}\n\nasync function generationReferenceInputItems(input) {\n const uploaded = imageInputItems(input.referenceFiles).map((item, index) => ({\n image: item.image_url,\n source: `用户上传参考图 ${index + 1}`,\n }));\n const refs = getGooglePlayReferences(input);\n const competitorRefs = getCompetitorGooglePlayReferences(input);\n const googlePlayUrls = [\n refs.icon ? { url: refs.icon, source: \"主产品 Google Play icon\" } : null,\n ...competitorRefs.map((item, index) => (item.icon ? { url: item.icon, source: `竞品 Google Play icon ${index + 1}` } : null)),\n ].filter(Boolean);\n\n const googlePlay = [];\n const errors = [];\n for (const item of googlePlayUrls) {\n try {\n googlePlay.push({\n image: await imageUrlToDataUrl(normalizeGoogleImageUrl(item.url)),\n source: item.source,\n });\n } catch (error) {\n errors.push({ url: item.url, source: item.source, error: error.message });\n console.warn(`Failed to inline generation reference image: ${item.url} ${error.message}`);\n }\n }\n\n const refsWithSources = [...uploaded, ...googlePlay].slice(0, 5);\n return {\n images: refsWithSources.filter((item) => item.image).map((item) => item.image),\n sources: refsWithSources.filter((item) => item.image).map((item) => item.source),\n errors,\n };\n}\n\nfunction getCompetitorGooglePlayReferences(input) {\n const packs =\n input.competitorGooglePlayReferences ||\n input.competitor_google_play_references ||\n input.competitor_visual_reference_pack ||\n input.googlePlay?.competitor_visual_reference_pack ||\n [];\n return packs.map((item) => ({\n icon: item.icon || item.app_icon || \"\",\n }));\n}\n\nfunction normalizeGoogleImageUrl(url) {\n if (!url || !url.includes(\"play-lh.googleusercontent.com\")) return url;\n if (/=[whs]\\d+/.test(url)) return url;\n return `${url}=w1024-h1024-rw`;\n}\n\nfunction getGooglePlayReferences(input) {\n const refs =\n input.googlePlayReferences ||\n input.google_play_visual_reference_pack ||\n input.visual_reference_pack ||\n input.googlePlay?.visual_reference_pack ||\n {};\n return {\n icon: refs.icon || refs.app_icon || \"\",\n featureGraphic: refs.featureGraphic || refs.feature_graphic || refs.feature || \"\",\n screenshots: refs.screenshots || refs.screen_shots || [],\n };\n}\n\nasync function imageUrlToDataUrl(url) {\n const response = await fetchWithFallbackBinary(url);\n const mime = response.mime || \"image/png\";\n return `data:${mime};base64,${response.buffer.toString(\"base64\")}`;\n}\n\nasync function runStructuredAnalysis({ text, images = [] }) {\n if (aiProvider === \"volcengine\") {\n const content = [\n { type: \"text\", text },\n ...images.map((image) => ({\n type: \"image_url\",\n image_url: { url: image.image_url },\n })),\n ];\n\n const response = await arkFetch(\"chat/completions\", {\n model: volcengineTextModel,\n messages: [{ role: \"user\", content }],\n temperature: 0.2,\n });\n\n return response.choices?.[0]?.message?.content || \"\";\n }\n\n if (aiProvider === \"aliyun\") {\n const content = [\n { type: \"text\", text },\n ...images.map((image) => ({\n type: \"image_url\",\n image_url: { url: image.image_url },\n })),\n ];\n\n const response = await dashscopeFetch(\"services/aigc/text-generation/generation\", {\n model: aliyunTextModel,\n input: { messages: [{ role: \"user\", content }] },\n parameters: { temperature: 0.2 },\n });\n\n return response.output?.text || response.output?.choices?.[0]?.message?.content || \"\";\n }\n\n if (aiProvider === \"kimi\") {\n const content = [\n { type: \"text\", text },\n ...images.map((image) => ({\n type: \"image_url\",\n image_url: { url: image.image_url },\n })),\n ];\n\n const response = await kimiFetch(\"chat/completions\", {\n model: kimiTextModel,\n messages: [{ role: \"user\", content }],\n temperature: 0.2,\n });\n\n return response.choices?.[0]?.message?.content || \"\";\n }\n\n if (aiProvider === \"deepseek\") {\n const { publicImages, skippedImages } = await deepseekImageInputs(images);\n const imageNotes = skippedImages.length\n ? `\\n\\nSome reference images are not passed as image_url because they are not stable public direct image URLs. Use these source notes as reference context, and do not claim pixel-level certainty for them:\\n${skippedImages\n .map((note, index) => `- Skipped reference ${index + 1}: ${note}`)\n .join(\"\\n\")}`\n : \"\";\n const content = [\n { type: \"text\", text: `${text}${imageNotes}` },\n ...publicImages.map((url) => ({\n type: \"image_url\",\n image_url: { url },\n })),\n ];\n\n let response;\n try {\n response = await deepseekFetch(\"chat/completions\", {\n model: deepseekTextModel,\n messages: [{ role: \"user\", content: publicImages.length ? content : `${text}${imageNotes}` }],\n temperature: 0.2,\n });\n } catch (error) {\n if (!publicImages.length || !/image_url|unknown variant|expected `text`|expected text/i.test(error.message)) throw error;\n response = await deepseekFetch(\"chat/completions\", {\n model: deepseekTextModel,\n messages: [\n {\n role: \"user\",\n content: `${text}\\n\\nImage URL inputs were validated as public direct image URLs, but the current DeepSeek endpoint rejected image_url content. Fall back to text-only analysis using these reference URLs as context:\\n${publicImages\n .map((url, index) => `- Reference ${index + 1}: ${url}`)\n .join(\"\\n\")}${imageNotes}`,\n },\n ],\n temperature: 0.2,\n });\n }\n\n return response.choices?.[0]?.message?.content || \"\";\n }\n\n const response = await openaiFetch(\"responses\", {\n model: textModel,\n input: [\n {\n role: \"user\",\n content: [{ type: \"input_text\", text }, ...images],\n },\n ],\n });\n\n return extractText(response);\n}\n\nfunction referenceImageNote(image = {}) {\n const value = image.image_url || image.url || image.name || \"uploaded image\";\n if (/^data:/i.test(value)) {\n const mime = value.slice(5, value.indexOf(\";\") > 0 ? value.indexOf(\";\") : 32);\n return `uploaded ${mime || \"image\"} reference`;\n }\n return String(value).slice(0, 240);\n}\n\nasync function deepseekImageInputs(images = []) {\n const publicImages = [];\n const skippedImages = [];\n for (const image of images.slice(0, 8)) {\n const candidate = image.public_url || image.image_url || image.url || \"\";\n if (!isPublicHttpUrl(candidate)) {\n skippedImages.push(`${referenceImageNote(image)}; not a public HTTP(S) URL`);\n continue;\n }\n const validation = await validatePublicImageUrl(candidate);\n if (validation.ok) {\n publicImages.push(candidate);\n } else {\n skippedImages.push(`${candidate}; ${validation.reason}`);\n }\n }\n return { publicImages, skippedImages };\n}\n\nfunction isPublicHttpUrl(value = \"\") {\n try {\n const url = new URL(value);\n if (!/^https?:$/.test(url.protocol)) return false;\n const hostname = url.hostname.toLowerCase();\n if (\n hostname === \"localhost\" ||\n hostname === \"127.0.0.1\" ||\n hostname === \"::1\" ||\n hostname.endsWith(\".local\") ||\n /^10\\./.test(hostname) ||\n /^192\\.168\\./.test(hostname) ||\n /^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(hostname)\n ) {\n return false;\n }\n return true;\n } catch {\n return false;\n }\n}\n\nasync function validatePublicImageUrl(url) {\n const check = async (method) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 10000);\n try {\n const response = await fetch(url, {\n method,\n headers: method === \"GET\" ? { Range: \"bytes=0-0\" } : undefined,\n signal: controller.signal,\n });\n const contentType = response.headers.get(\"content-type\") || \"\";\n if (response.ok && /^image\\//i.test(contentType)) return { ok: true, contentType };\n return { ok: false, reason: `Content-Type is \"${contentType || \"missing\"}\"` };\n } finally {\n clearTimeout(timer);\n }\n };\n\n try {\n const head = await check(\"HEAD\");\n if (head.ok) return head;\n } catch (error) {\n // Some image CDNs reject HEAD; try a tiny ranged GET below.\n }\n\n try {\n return await check(\"GET\");\n } catch (error) {\n return { ok: false, reason: error.name === \"AbortError\" ? \"validation timed out\" : error.message };\n }\n}\n\nfunction buildWorkflowPayload(input) {\n return {\n product: input.product || \"未填写\",\n competitors: input.competitors || \"无\",\n platform: input.platform || \"Google Ads\",\n imported_platform_rules: input.platformRules || \"\",\n emotion_target: input.emotion || [],\n text_spec:\n input.textEnabled === \"是\"\n ? { enabled: true, text: input.badgeText || \"\", position: \"右下角角标\", max_chars: 5 }\n : { enabled: false, no_text: true },\n emphasis: input.emphasis || \"\",\n sensitive: input.sensitive || \"\",\n reference_notes: input.reference || \"\",\n uploaded_files: (input.referenceFiles || []).slice(0, 2).map((file) => ({\n name: file.name,\n type: file.type,\n size: file.size,\n })),\n google_play_profile: input.googlePlayProfile || null,\n google_play_visual_reference_pack: input.googlePlayReferences || null,\n competitor_google_play_profiles: input.competitorGooglePlayProfiles || [],\n competitor_google_play_icons: (input.competitorGooglePlayReferences || []).map((item) => ({\n app_id: item.app_id || \"\",\n title: item.title || item.app_title || \"\",\n icon: item.icon || \"\",\n })),\n variant_count: Number(input.count || 2),\n directions: input.directions || [\"点击强化\"],\n };\n}\n\nasync function analyzeWorkflow(input) {\n const workflow = buildWorkflowPayload(input);\n if (appConfig.prompt_templates?.s2_analysis) {\n const configuredPrompt = renderTemplate(appConfig.prompt_templates.s2_analysis, {\n WORKFLOW_JSON: workflow,\n PROTOCOL_SCHEMA_JSON: appConfig.protocol_schema || {},\n OUTPUT_SCHEMA_JSON: appConfig.output_schemas?.s2_analysis || {},\n PROMPT_FIELD_NAMES_JSON: appConfig.prompt_field_names || [],\n });\n const text = await runStructuredAnalysis({\n text: configuredPrompt,\n images: await visualReferenceInputItems(input),\n });\n return normalizeAnalysisResult(parseJsonLoose(text) || { raw_text: text });\n }\n const text = await runStructuredAnalysis({\n text: `你是广告投放 Icon 素材生成 Agent 的 S2 分析器。\n你的任务:基于 S1 输入、Google Play 主产品资料、主产品 icon / feature graphic / screenshots、竞品 icon、用户上传参考图,输出产品/竞品/icon 的结构化分析。\n注意:平台约束来自 imported_platform_rules,由前端可编辑规则导入维护。你不要分析、改写或生成平台政策约束。\n\n只返回 JSON,不要 Markdown,不要解释。\n\n必须输出以下 JSON 结构,字段缺失时也要给出合理推断,不要留空:\n{\n \"product_analysis\": {\n \"core_gameplay\": \"\",\n \"theme\": \"\",\n \"art_style\": \"\",\n \"visual_style\": \"\",\n \"target_audience\": \"\",\n \"core_selling_points\": [],\n \"ad_expressible_points\": []\n },\n \"product_icon_analysis\": {\n \"image_content\": \"\",\n \"main_subject\": \"\",\n \"visual_style\": \"\",\n \"color_features\": \"\",\n \"composition_features\": \"\",\n \"text_features\": \"\",\n \"small_size_readability\": \"\",\n \"identity_cues\": []\n },\n \"competitor_icon_analysis\": [\n {\n \"app_title\": \"\",\n \"app_id\": \"\",\n \"image_content\": \"\",\n \"main_subject\": \"\",\n \"visual_style\": \"\",\n \"color_features\": \"\",\n \"composition_features\": \"\",\n \"text_features\": \"\",\n \"small_size_readability\": \"\",\n \"notable_click_drivers\": []\n }\n ],\n \"common_icon_signature\": {\n \"subject_pattern\": \"\",\n \"composition_pattern\": \"\",\n \"color_pattern\": \"\",\n \"rendering_pattern\": \"\",\n \"emotion_pattern\": \"\",\n \"text_or_badge_pattern\": \"\",\n \"market_effective_formula\": []\n },\n \"differentiation_opportunities\": {\n \"keep\": [],\n \"strengthen\": [],\n \"avoid_copying\": [],\n \"possible_breakthrough_points\": []\n },\n \"generation_prompt_fields\": {\n \"game_genre\": \"\",\n \"setting\": \"\",\n \"core_narrative\": \"\",\n \"scene_context\": \"\",\n \"subject_type\": \"\",\n \"archetype\": \"\",\n \"pose\": \"\",\n \"facial_expression\": \"\",\n \"camera_angle\": \"\",\n \"eye_contact\": \"\",\n \"render_style\": \"\",\n \"detail_level\": \"\",\n \"lighting_style\": \"\",\n \"color_strategy\": \"\",\n \"surface_finish\": \"\",\n \"background_story\": \"\",\n \"curiosity_trigger\": \"\",\n \"urgency_trigger\": \"\",\n \"emotional_reaction_trigger\": \"\",\n \"dramatic_visual_tension\": \"\",\n \"value_desire\": \"\",\n \"symbol_set\": \"\"\n },\n \"locked_insights_for_next_stage\": {\n \"identity_anchor\": \"\",\n \"reference_priority\": [\"user_uploaded_reference\", \"product_icon\", \"competitor_icons\"],\n \"must_include_candidates\": [],\n \"must_not_include_candidates\": [],\n \"style_keywords\": [],\n \"risk_flags\": []\n }\n}\n\n生成规则:\n1. S2 只做理解与分析,不生成图片,不输出生成 prompt。\n2. 必须分别分析主产品 icon 和竞品 icon。\n3. 不要输出 platform_constraints,不要判断平台政策,不要改写 imported_platform_rules。\n4. generation_prompt_fields 的字段名必须与 S3 提示词模板占位符保持一致;内容必须从 S1 输入、Google Play 信息、图片理解和竞品 icon 分析中提炼,不要留空。\n5. reference_priority 必须固定为:user_uploaded_reference > product_icon > competitor_icons。\n6. 不要输出 seed,不要输出内部规则。\n\n输入:\n${JSON.stringify(workflow, null, 2)}`,\n images: await visualReferenceInputItems(input),\n });\n\n return parseJsonLoose(text) || { raw_text: text };\n}\n\nasync function optimizePrompts(input) {\n const workflow = buildWorkflowPayload(input);\n const count = Math.max(1, Math.min(2, Number(input.count || workflow.variant_count || 2)));\n const directions = (input.directions || workflow.directions || [\"点击强化\"]).slice(0, 2);\n const text = await runStructuredAnalysis({\n text: `你是移动游戏广告 Icon 的高级提示词创意总监。\n任务:不要直接生成最终 prompt,而是基于 S1/S2 信息,为每个方案生成可填入固定模板的 generation_prompt_fields。\n\n只返回 JSON,不要 Markdown,不要解释。输出结构:\n{\n \"prompt_plan\": [\n {\n \"variant_tag\": \"保守/点击强化/极致夸张\",\n \"creative_rationale\": \"\",\n \"generation_prompt_fields\": {\n \"game_genre\": \"\",\n \"setting\": \"\",\n \"core_narrative\": \"\",\n \"scene_context\": \"\",\n \"subject_type\": \"\",\n \"archetype\": \"\",\n \"pose\": \"\",\n \"facial_expression\": \"\",\n \"camera_angle\": \"\",\n \"eye_contact\": \"\",\n \"render_style\": \"\",\n \"detail_level\": \"\",\n \"lighting_style\": \"\",\n \"color_strategy\": \"\",\n \"surface_finish\": \"\",\n \"background_story\": \"\",\n \"curiosity_trigger\": \"\",\n \"urgency_trigger\": \"\",\n \"emotional_reaction_trigger\": \"\",\n \"dramatic_visual_tension\": \"\",\n \"value_desire\": \"\",\n \"symbol_set\": \"\"\n }\n }\n ]\n}\n\n硬规则:\n1. prompt_plan 数量必须等于 ${count}。\n2. 每个 variant_tag 必须来自用户选择方向:${directions.join(\" / \")}。\n3. 各方案必须明显不同:主体、姿态、光影、色彩策略或点击机制至少 3 项不同。\n4. 必须把 S1/S2 的产品信息、icon 分析、竞品共性、情绪目标转译成具体画面语言。\n5. 不要输出平台政策,平台约束由模板 STRICT CONSTRAINTS 注入。\n6. 不要输出 seed,不要输出最终整段 prompt。\n\n输入:\n${JSON.stringify(\n {\n workflow,\n model_analysis: input.modelAnalysis || null,\n reference_summary: {\n has_user_uploads: Boolean((input.referenceFiles || []).length),\n has_product_icon: Boolean(input.googlePlayReferences?.icon),\n competitor_icon_count: (input.competitorGooglePlayReferences || []).filter((item) => item.icon).length,\n },\n },\n null,\n 2,\n)}`,\n });\n const parsed = parseJsonLoose(text);\n if (parsed?.prompt_plan) return parsed.prompt_plan.slice(0, count);\n return [];\n}\n\nfunction normalizeAnalysisResult(result = {}) {\n const promptFields = appConfig.prompt_field_names || [];\n result.ICON_CREATIVE_PROTOCOL = result.ICON_CREATIVE_PROTOCOL || appConfig.protocol_schema?.ICON_CREATIVE_PROTOCOL || {};\n result.generation_prompt_fields = result.generation_prompt_fields || {};\n for (const field of promptFields) {\n if (result.generation_prompt_fields[field] === undefined) result.generation_prompt_fields[field] = \"\";\n }\n result.locked_insights_for_next_stage = result.locked_insights_for_next_stage || {};\n result.locked_insights_for_next_stage.reference_priority =\n result.locked_insights_for_next_stage.reference_priority || appConfig.reference_policy?.priority || [\"user_uploaded_reference\", \"product_icon\", \"competitor_icons\"];\n return result;\n}\n\nfunction normalizePromptPlan(plan = [], directions = [], count = 1) {\n const promptFields = appConfig.prompt_field_names || [];\n return Array.from({ length: count }).map((_, index) => {\n const item = plan[index] || {};\n const fields = item.generation_prompt_fields || {};\n for (const field of promptFields) {\n if (fields[field] === undefined) fields[field] = \"\";\n }\n return {\n variant_tag: item.variant_tag || directions[index % Math.max(1, directions.length)] || \"点击强化\",\n creative_rationale: item.creative_rationale || \"\",\n generation_mode: item.generation_mode || \"\",\n reference_image_policy: item.reference_image_policy || {\n use_user_uploaded_references: true,\n use_product_icon_reference: true,\n use_competitor_icon_references: true,\n reference_priority: appConfig.reference_policy?.priority || [\"user_uploaded_reference\", \"product_icon\", \"competitor_icons\"],\n },\n generation_prompt_fields: fields,\n final_prompt: item.final_prompt || item.prompt || item.generation_prompt || \"\",\n prompt: item.final_prompt || item.prompt || item.generation_prompt || \"\",\n negative_constraints: item.negative_constraints || [],\n quality_checkpoints: item.quality_checkpoints || [],\n };\n });\n}\n\nasync function optimizePromptsConfigured(input) {\n if (!appConfig.prompt_templates?.s4_prompt_plan) return optimizePrompts(input);\n const workflow = buildWorkflowPayload(input);\n const count = Math.max(1, Math.min(2, Number(input.count || workflow.variant_count || 2)));\n const directions = (input.directions || workflow.directions || [\"点击强化\"]).slice(0, 2);\n const s4Input = {\n workflow,\n model_analysis: input.modelAnalysis || null,\n icon_creative_protocol: input.modelAnalysis?.ICON_CREATIVE_PROTOCOL || null,\n selected_directions: directions,\n variant_count: count,\n reference_summary: {\n has_user_uploads: Boolean((input.referenceFiles || []).length),\n uploaded_reference_public_urls: (input.referenceFiles || []).map((file) => file.publicUrl || file.url || \"\").filter(Boolean),\n has_product_icon: Boolean(input.googlePlayReferences?.icon),\n product_icon_url: input.googlePlayReferences?.icon || \"\",\n competitor_icon_count: (input.competitorGooglePlayReferences || []).filter((item) => item.icon).length,\n competitor_icon_urls: (input.competitorGooglePlayReferences || []).map((item) => item.icon).filter(Boolean),\n },\n };\n const configuredPrompt = renderTemplate(appConfig.prompt_templates.s4_prompt_plan, {\n S4_INPUT_JSON: s4Input,\n OUTPUT_SCHEMA_JSON: appConfig.output_schemas?.s4_prompt_plan || {},\n PROMPT_FIELD_NAMES_JSON: appConfig.prompt_field_names || [],\n COUNT: String(count),\n DIRECTIONS: directions.join(\" / \"),\n });\n const text = await runStructuredAnalysis({ text: configuredPrompt });\n const parsed = parseJsonLoose(text);\n if (parsed?.prompt_plan) return normalizePromptPlan(parsed.prompt_plan, directions, count);\n return [];\n}\n\nasync function checkPrompts(input) {\n const promptJson = Array.isArray(input.promptJson) ? input.promptJson : [];\n if (!promptJson.length) throw new Error(\"No prompt_json found for S4 prompt check.\");\n const checkInput = {\n platform: input.platform || \"Google Ads\",\n product: input.product || \"\",\n prompt_count: promptJson.length,\n prompt_items: promptJson.map((prompt, index) => ({\n prompt_id: prompt.prompt_id || `prompt_${index + 1}`,\n variant_tag: prompt.variant_tag || \"\",\n prompt_text: prompt.prompt_text || prompt.prompt || \"\",\n must_include: prompt.constraints?.must_include || [],\n must_not_include: prompt.constraints?.must_not_include || [],\n text_spec: prompt.text_spec || {},\n })),\n };\n const configuredPrompt = renderTemplate(appConfig.prompt_templates?.s4_prompt_check || \"\", {\n PROMPT_CHECK_INPUT_JSON: checkInput,\n OUTPUT_SCHEMA_JSON: appConfig.output_schemas?.s4_prompt_check || {},\n });\n const text = await runStructuredAnalysis({ text: configuredPrompt || JSON.stringify(checkInput, null, 2) });\n const parsed = parseJsonLoose(text) || { raw_text: text };\n return normalizePromptCheck(parsed, promptJson);\n}\n\nfunction normalizePromptCheck(result = {}, promptJson = []) {\n const checkedItems = Array.isArray(result.checked_items) ? result.checked_items : [];\n const normalizedItems = promptJson.map((prompt, index) => {\n const item = checkedItems[index] || checkedItems.find((entry) => entry.prompt_id === prompt.prompt_id) || {};\n return {\n prompt_id: item.prompt_id || prompt.prompt_id || `prompt_${index + 1}`,\n variant_tag: item.variant_tag || prompt.variant_tag || \"\",\n status: [\"pass\", \"warning\", \"block\"].includes(item.status) ? item.status : \"warning\",\n risk_level: item.risk_level || (item.status === \"block\" ? \"high\" : item.status === \"warning\" ? \"medium\" : \"none\"),\n risk_categories: Array.isArray(item.risk_categories) ? item.risk_categories : [],\n flagged_terms: Array.isArray(item.flagged_terms) ? item.flagged_terms : [],\n reason: item.reason || \"\",\n rewrite_suggestions: Array.isArray(item.rewrite_suggestions) ? item.rewrite_suggestions : [],\n };\n });\n const hasBlock = normalizedItems.some((item) => item.status === \"block\");\n const hasWarning = normalizedItems.some((item) => item.status === \"warning\");\n return {\n overall_status: hasBlock ? \"block\" : hasWarning ? \"warning\" : result.overall_status || \"pass\",\n summary: result.summary || (hasBlock ? \"存在高风险提示词,建议修改后再生成。\" : hasWarning ? \"存在可能触发过滤的措辞,建议优化。\" : \"未发现明显高风险禁用词。\"),\n checked_items: normalizedItems,\n global_rewrite_suggestions: Array.isArray(result.global_rewrite_suggestions) ? result.global_rewrite_suggestions : [],\n };\n}\n\nasync function googlePlayLookup(input) {\n const product = String(input.product || \"\").trim();\n const competitors = String(input.competitors || \"\")\n .split(/[,\\n,、]/)\n .map((item) => item.trim())\n .filter(Boolean)\n .slice(0, 3);\n\n if (!product) throw new Error(\"请先填写待投产品名称。\");\n\n const apps = [];\n apps.push(await googlePlayLookupOne(product, \"product\"));\n for (const competitor of competitors) {\n apps.push(await googlePlayLookupOne(competitor, \"competitor\"));\n }\n\n const primary = apps[0];\n return {\n product_profile: primary.profile,\n competitor_profiles: apps.slice(1).map((app) => app.profile),\n visual_reference_pack: primary.visual_reference_pack,\n competitor_visual_reference_pack: apps.slice(1).map((app) => ({\n app_id: app.profile.app_id,\n title: app.profile.app_title,\n icon: app.visual_reference_pack.icon,\n screenshots: app.visual_reference_pack.screenshots?.slice(0, 4) || [],\n })),\n source_apps: apps,\n };\n}\n\nasync function googlePlayLookupOne(query, role) {\n const appId = query.includes(\".\") && !/\\s/.test(query) ? query : await findGooglePlayAppId(query);\n const detailUrl = `https://play.google.com/store/apps/details?id=${encodeURIComponent(appId)}&hl=en&gl=US`;\n const html = await fetchTextWithFallback(detailUrl);\n return parseGooglePlayDetail(html, appId, role, detailUrl);\n}\n\nasync function findGooglePlayAppId(query) {\n const searchUrl = `https://play.google.com/store/search?q=${encodeURIComponent(query)}&c=apps&hl=en&gl=US`;\n const html = await fetchTextWithFallback(searchUrl);\n const ids = unique(\n [...html.matchAll(/\\/store\\/apps\\/details\\?id\\\\?=([a-zA-Z0-9._]+)/g)].map((match) =>\n htmlDecode(match[1]).replace(/\\\\u0026.*$/, \"\"),\n ),\n );\n if (!ids.length) throw new Error(`Google Play 未找到应用:${query}`);\n return ids[0];\n}\n\nfunction parseGooglePlayDetail(html, appId, role, detailUrl) {\n const title =\n matchMeta(html, \"og:title\")?.replace(/\\s*-\\s*(Android\\s+)?Apps on Google Play$/i, \"\") ||\n matchTag(html, \"title\")?.replace(/\\s*-\\s*(Android\\s+)?Apps on Google Play$/i, \"\") ||\n appId;\n const shortDescription = matchMeta(html, \"og:description\") || \"\";\n const developer = extractNearAppIdDeveloper(html, appId) || \"\";\n const installs = firstMatch(html, /\"([0-9,]+\\+)\"\\s*,null,\"\"\\s*,null,null,null,null,\\[null,2,\\[500,1024\\]/) || \"\";\n const category = firstMatch(html, /\\],\"([^\"]+)\",null,null,\\[null,\\[\\[0,\"USD\"/) || \"\";\n const rating = firstMatch(html, /\\[\"([0-9.]+)\",[0-9.]+\\],\"[^\"]+\"/) || \"\";\n const media = extractGooglePlayMedia(html);\n const icon = matchMeta(html, \"og:image\") || media.find((item) => item.width === 512 && item.height === 512)?.url || media[0]?.url || \"\";\n const screenshots = media\n .filter((item) => item.url !== icon && item.width >= 700 && item.height >= 700)\n .sort((a, b) => b.width * b.height - a.width * a.height)\n .map((item) => item.url)\n .slice(0, 8);\n const featureGraphic =\n media.find((item) => item.url !== icon && item.width >= 900 && item.height >= 450 && item.width > item.height)?.url ||\n screenshots.find((url) => url) ||\n \"\";\n const cleanScreenshots = screenshots.filter((url) => url !== featureGraphic).slice(0, 8);\n\n return {\n profile: {\n role,\n app_id: appId,\n app_title: htmlDecode(title),\n developer: htmlDecode(developer),\n category: htmlDecode(category),\n rating,\n installs,\n short_description: htmlDecode(shortDescription),\n detail_url: detailUrl,\n },\n visual_reference_pack: {\n icon,\n featureGraphic,\n screenshots: cleanScreenshots,\n visual_rules: summarizeVisualRules(media, icon, cleanScreenshots),\n },\n };\n}\n\nfunction extractGooglePlayMedia(html) {\n const media = [];\n const regex = /\\[null,2,\\[(\\d+),(\\d+)\\],\\[null,null,\"(https:\\/\\/play-lh\\.googleusercontent\\.com\\/[^\"]+)\"/g;\n let match;\n while ((match = regex.exec(html))) {\n media.push({\n width: Number(match[1]),\n height: Number(match[2]),\n url: htmlDecode(match[3]),\n });\n }\n const metaImage = matchMeta(html, \"og:image\");\n if (metaImage) media.unshift({ width: 512, height: 512, url: metaImage });\n const seen = new Set();\n return media.filter((item) => {\n if (seen.has(item.url)) return false;\n seen.add(item.url);\n return true;\n });\n}\n\nfunction summarizeVisualRules(media, icon, screenshots) {\n const iconItem = media.find((item) => item.url === icon);\n const landscape = media.filter((item) => item.width > item.height).length;\n const portrait = media.filter((item) => item.height > item.width).length;\n return [\n iconItem ? `icon 素材尺寸约 ${iconItem.width}x${iconItem.height}` : \"已抓取 icon 默认参考图\",\n screenshots.length ? `抓取到 ${screenshots.length} 张截图/主图素材` : \"截图素材较少,需依赖 icon 和用户素材\",\n landscape >= portrait ? \"视觉素材以横版展示为主\" : \"视觉素材以竖版展示为主\",\n \"图片来源为 Google Play 公开素材,仅作为风格与构图参考\",\n ];\n}\n\nfunction matchMeta(html, property) {\n const escaped = property.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return htmlDecode(\n firstMatch(html, new RegExp(`<meta[^>]+(?:property|name)=[\"']${escaped}[\"'][^>]+content=[\"']([^\"']+)[\"']`, \"i\")) ||\n firstMatch(html, new RegExp(`<meta[^>]+content=[\"']([^\"']+)[\"'][^>]+(?:property|name)=[\"']${escaped}[\"']`, \"i\")) ||\n \"\",\n );\n}\n\nfunction matchTag(html, tag) {\n return htmlDecode(firstMatch(html, new RegExp(`<${tag}[^>]*>([\\\\s\\\\S]*?)<\\\\/${tag}>`, \"i\")) || \"\");\n}\n\nfunction firstMatch(text, regex) {\n const match = text.match(regex);\n return match ? match[1] : \"\";\n}\n\nfunction extractNearAppIdDeveloper(html, appId) {\n const index = html.indexOf(`\"${appId}\"`);\n if (index === -1) return \"\";\n const slice = html.slice(index, index + 20000);\n return firstMatch(slice, /],\"([^\"]+)\",\\[\"[0-9.]+\",[0-9.]+\\],\"[^\"]+\"/) || \"\";\n}\n\nasync function makeSmokeReferenceDataUrl() {\n const buffer = await sharp({\n create: {\n width: 32,\n height: 32,\n channels: 4,\n background: \"#2563eb\",\n },\n })\n .composite([\n {\n input: Buffer.from('<svg width=\"32\" height=\"32\"><circle cx=\"16\" cy=\"16\" r=\"9\" fill=\"#facc15\"/></svg>'),\n top: 0,\n left: 0,\n },\n ])\n .png()\n .toBuffer();\n return `data:image/png;base64,${buffer.toString(\"base64\")}`;\n}\n\nfunction smokeBasePayload(uploaded, dataUrl = \"\") {\n return {\n product: \"Test Kingdom Builder\",\n competitors: \"Township\",\n platform: \"Google Ads\",\n platformRules: \"- No misleading claims\\n- No gore, nudity, or copyrighted competitor logos\\n- Icon must be readable at 64px\",\n emotion: [\"好奇\", \"成就感\"],\n textEnabled: \"否\",\n badgeText: \"\",\n reference: \"Use uploaded reference as highest priority style cue.\",\n referenceFiles: uploaded\n ? [\n {\n name: uploaded.name,\n type: uploaded.type,\n size: uploaded.size,\n url: uploaded.url,\n publicUrl: uploaded.public_url,\n dataUrl,\n },\n ]\n : [],\n googlePlayProfile: {\n app_title: \"Test Kingdom Builder\",\n app_id: \"test.kingdom.builder\",\n category: \"Simulation\",\n developer: \"Test Studio\",\n short_description: \"Build a medieval town, collect resources, and unlock rewards.\",\n },\n googlePlayReferences: {\n icon: uploaded?.public_url || \"\",\n featureGraphic: \"\",\n screenshots: [],\n },\n competitorGooglePlayProfiles: [\n {\n app_title: \"Township\",\n app_id: \"com.playrix.township\",\n category: \"Simulation\",\n },\n ],\n competitorGooglePlayReferences: [],\n count: 1,\n directions: [\"点击强化\"],\n promptJson: [],\n generatedImages: [],\n };\n}\n\nfunction summarizeForSmoke(value, maxChars = 7000) {\n const text = JSON.stringify(value, null, 2);\n return text.length > maxChars ? { truncated: true, json_preview: text.slice(0, maxChars) } : value;\n}\n\nasync function runSmokeTest(mode, req) {\n const normalizedMode = [\"quick\", \"ai\", \"full\"].includes(mode) ? mode : \"quick\";\n const startedAt = new Date().toISOString();\n const steps = [];\n const io = {};\n const mark = (name, status, detail = {}) => steps.push({ name, status, ...detail });\n\n mark(\"静态页面\", \"passed\", { message: \"index.html 由当前服务托管\" });\n mark(\"模型配置\", providerHasKey(aiProvider) && providerHasKey(imageProvider) ? \"passed\" : \"warning\", {\n text_provider: aiProvider,\n text_model: aiProvider === \"deepseek\" ? deepseekTextModel : textModel,\n image_provider: imageProvider,\n image_model: imageProvider === \"volcengine\" ? volcengineImageModel : imageModel,\n });\n mark(\"配置文件\", appConfig.protocol_schema?.ICON_CREATIVE_PROTOCOL && appConfig.prompt_templates?.s2_analysis ? \"passed\" : \"failed\", {\n has_protocol_schema: Boolean(appConfig.protocol_schema?.ICON_CREATIVE_PROTOCOL),\n has_s2_template: Boolean(appConfig.prompt_templates?.s2_analysis),\n has_s4_template: Boolean(appConfig.prompt_templates?.s4_prompt_plan),\n });\n\n const uploadInput = {\n name: \"smoke-reference.png\",\n type: \"image/png\",\n dataUrl: await makeSmokeReferenceDataUrl(),\n };\n const uploaded = await uploadReferenceImage(uploadInput, req);\n mark(\"参考图上传\", \"passed\", {\n public_url: uploaded.public_url,\n type: uploaded.type,\n size: uploaded.size,\n });\n\n const payload = smokeBasePayload(uploaded, uploadInput.dataUrl);\n io.quick = {\n upload_input: { name: uploadInput.name, type: uploadInput.type, data_url_length: uploadInput.dataUrl.length },\n upload_output: uploaded,\n };\n\n let analysis = null;\n let promptPlan = null;\n if (normalizedMode === \"ai\" || normalizedMode === \"full\") {\n io.analyze = { input: payload };\n analysis = await analyzeWorkflow(payload);\n io.analyze.output = summarizeForSmoke(analysis);\n mark(\"S2 AI 分析\", analysis?.generation_prompt_fields ? \"passed\" : \"failed\", {\n has_protocol: Boolean(analysis?.ICON_CREATIVE_PROTOCOL),\n has_generation_prompt_fields: Boolean(analysis?.generation_prompt_fields),\n });\n\n const optimizeInput = { ...payload, modelAnalysis: analysis };\n io.prompt_plan = { input: optimizeInput };\n promptPlan = await optimizePromptsConfigured(optimizeInput);\n io.prompt_plan.output = summarizeForSmoke(promptPlan);\n mark(\"S4 提示词计划\", Array.isArray(promptPlan) && promptPlan.length ? \"passed\" : \"failed\", {\n prompt_count: Array.isArray(promptPlan) ? promptPlan.length : 0,\n has_final_prompt: Boolean(promptPlan?.[0]?.final_prompt || promptPlan?.[0]?.prompt),\n });\n }\n\n if (normalizedMode === \"full\") {\n const promptText =\n promptPlan?.[0]?.final_prompt ||\n promptPlan?.[0]?.prompt ||\n \"Create a production-ready transparent-background mobile game app icon, single golden castle emblem, high contrast, readable at 64px.\";\n const generateInput = {\n ...payload,\n promptJson: [\n {\n prompt_id: \"smoke_prompt_1\",\n variant_tag: \"点击强化\",\n prompt_text: promptText,\n generation: { source: \"smoke-test-ui\", mode: \"image_to_image_with_prompt\" },\n },\n ],\n };\n io.generate = { input: generateInput };\n const generatedImages = await generateIcons(generateInput);\n io.generate.output = generatedImages;\n mark(\"S5 Icon 生成\", generatedImages.length ? \"passed\" : \"failed\", {\n image_count: generatedImages.length,\n first_image_url: generatedImages[0]?.url || \"\",\n first_scene_url: generatedImages[0]?.scene_url || \"\",\n });\n\n const exportInput = {\n ...payload,\n generatedImages,\n selectedImageIds: [generatedImages[0].image_id],\n sizes: [1024, 512, 256, 128, 64],\n };\n io.export = { input: exportInput };\n const exportResult = await exportIcons(exportInput);\n io.export.output = exportResult;\n mark(\"S8 多尺寸导出\", exportResult?.zip_url ? \"passed\" : \"failed\", {\n zip_url: exportResult?.zip_url || \"\",\n file_count: exportResult?.files?.length || 0,\n });\n }\n\n return {\n ok: steps.every((step) => step.status !== \"failed\"),\n mode: normalizedMode,\n started_at: startedAt,\n finished_at: new Date().toISOString(),\n base_url: publicBaseUrl(req),\n steps,\n io,\n };\n}\n\nfunction buildIconPrompt(input, index) {\n const workflow = buildWorkflowPayload(input);\n const direction = workflow.directions[index % workflow.directions.length] || \"点击强化\";\n const textRule = workflow.text_spec.enabled\n ? `Include a tiny bottom-right badge with the exact text \"${workflow.text_spec.text}\", max 5 characters.`\n : \"No text, no letters, no numbers, no watermark.\";\n\n return [\n \"Create a production-ready mobile app icon for paid app install ads.\",\n `Product: ${workflow.product}. Competitors: ${workflow.competitors}.`,\n `Platform: ${workflow.platform}. Creative direction: ${direction}.`,\n `Emotion target: ${workflow.emotion_target.join(\", \") || \"curiosity and high click appeal\"}.`,\n `Must emphasize: ${workflow.emphasis || \"clear core product identity and strong small-size recognizability\"}.`,\n `Must avoid: ${workflow.sensitive || \"misleading claims, gore, nudity, copyrighted logos, clutter\"}.`,\n `Reference notes: ${workflow.reference_notes || \"use uploaded references only as style inspiration, do not copy exact protected icons\"}.`,\n \"Icon constraints: one clear central subject, square 1:1 composition, transparent background with alpha channel, high contrast silhouette, readable at 64px, polished app-store quality, no border frame.\",\n \"Style: premium 3D-rendered app icon, vivid lighting, tactile materials, clean shape language, memorable focal point.\",\n textRule,\n \"Output only the icon artwork, transparent-background PNG.\",\n ].join(\"\\n\");\n}\n\nfunction getPromptJsonList(input) {\n const promptJson = Array.isArray(input.promptJson) ? input.promptJson : [];\n if (promptJson.length) return promptJson;\n\n const count = Math.max(1, Math.min(2, Number(input.count || 2)));\n return Array.from({ length: count }).map((_, index) => ({\n prompt_id: `server_fallback_prompt_${index + 1}`,\n variant_tag: (input.directions || [])[index % Math.max(1, (input.directions || []).length)] || \"点击强化\",\n prompt_text: buildIconPrompt(input, index),\n generation: { source: \"server fallback prompt_json\" },\n }));\n}\n\nfunction promptTextFromPromptJson(promptJson, input, index) {\n if (promptJson?.prompt_text) return promptJson.prompt_text;\n if (promptJson?.prompt) return promptJson.prompt;\n\n return [\n `Create a production-ready mobile app icon from S4 prompt_json ${promptJson?.prompt_id || index + 1}.`,\n `Variant: ${promptJson?.variant_tag || \"点击强化\"}.`,\n `Subject: ${promptJson?.subject || input.product || \"未填写\"}.`,\n `Style: ${JSON.stringify(promptJson?.style || \"premium app icon\")}.`,\n `Composition: ${JSON.stringify(promptJson?.composition || \"centered single subject\")}.`,\n `Emotion: ${JSON.stringify(promptJson?.emotion || input.emotion || [])}.`,\n `Constraints: ${JSON.stringify(promptJson?.constraints || {})}.`,\n promptJson?.text_spec?.enabled ? `Text badge: ${promptJson.text_spec.text}, bottom-right.` : \"No text, no letters, no numbers, no watermark.\",\n \"Icon constraints: square 1:1, transparent background with alpha channel, high contrast, readable at 64px, output only icon artwork.\",\n ].join(\"\\n\");\n}\n\nasync function generateIcons(input) {\n const promptJsonList = getPromptJsonList(input);\n const count = Math.max(1, Math.min(2, Number(input.count || promptJsonList.length || 2)));\n const images = [];\n const referencePack = await generationReferenceInputItems(input);\n const referenceImages = referencePack.images;\n const generationMode = referenceImages.length ? \"image_to_image_with_prompt\" : \"text_to_image\";\n\n for (let index = 0; index < count; index += 1) {\n const promptJson = promptJsonList[index] || promptJsonList[promptJsonList.length - 1];\n const prompt = promptTextFromPromptJson(promptJson, input, index);\n const image = await generateImage(prompt, referenceImages);\n\n const id = `icon_${Date.now()}_${index + 1}_${crypto.randomBytes(3).toString(\"hex\")}`;\n const filename = `${id}.png`;\n const iconBuffer = await makeIconTransparentPng(await resolveImageBuffer(image));\n fs.writeFileSync(path.join(outputDir, filename), iconBuffer);\n const sceneFilename = `${id}_scene.svg`;\n fs.writeFileSync(\n path.join(outputDir, sceneFilename),\n buildSceneTemplateSvg({\n iconBuffer,\n productName: input.googlePlayProfile?.app_title || input.product || \"游戏名称\",\n companyName: input.googlePlayProfile?.developer || \"Your Company\",\n platform: input.platform || \"Google Ads\",\n variant: promptJson?.variant_tag || (input.directions || [])[index % (input.directions || []).length] || \"点击强化\",\n }),\n );\n images.push({\n image_id: id,\n scene_image_id: `${id}_scene`,\n prompt_id: promptJson?.prompt_id || `server_fallback_prompt_${index + 1}`,\n variant_tag: promptJson?.variant_tag || (input.directions || [])[index % (input.directions || []).length] || \"点击强化\",\n url: `/generated/${filename}`,\n scene_url: `/generated/${sceneFilename}`,\n prompt_summary: prompt.split(\"\\n\").slice(0, 5).join(\" \"),\n prompt_source: promptJson?.generation?.source || \"S4 prompt_json\",\n reference_image_count: referenceImages.length,\n reference_image_sources: referencePack.sources,\n reference_image_errors: referencePack.errors,\n generation_mode: generationMode,\n scene_generation_mode: \"scene_template_composite\",\n });\n }\n\n return images;\n}\n\nasync function regenerateIcon(input) {\n const image = input.image || {};\n const currentCount = Number(image.regenerate_count || 0);\n if (currentCount >= 2) {\n throw new Error(\"该 icon 已达到最多 2 次重生限制。请回到 S3 修改提示词模板或最终提示词后重新生成。\");\n }\n\n const promptJson = input.promptJson || {};\n const prompt = String(input.promptText || promptJson.prompt_text || image.prompt_text || \"\").trim();\n if (!prompt) throw new Error(\"缺少用于重生的 prompt。\");\n\n const referencePack = await generationReferenceInputItems(input);\n const referenceImages = referencePack.images;\n const generationMode = referenceImages.length ? \"image_to_image_with_prompt\" : \"text_to_image\";\n const result = await generateImage(prompt, referenceImages);\n const iconBuffer = await makeIconTransparentPng(await resolveImageBuffer(result));\n\n const id = `icon_${Date.now()}_regen_${crypto.randomBytes(3).toString(\"hex\")}`;\n const filename = `${id}.png`;\n fs.writeFileSync(path.join(outputDir, filename), iconBuffer);\n\n const sceneFilename = `${id}_scene.svg`;\n fs.writeFileSync(\n path.join(outputDir, sceneFilename),\n buildSceneTemplateSvg({\n iconBuffer,\n productName: input.googlePlayProfile?.app_title || input.product || \"游戏名称\",\n companyName: input.googlePlayProfile?.developer || \"Your Company\",\n platform: input.platform || \"Google Ads\",\n variant: image.variant_tag || promptJson.variant_tag || \"重生\",\n }),\n );\n\n return {\n image_id: id,\n scene_image_id: `${id}_scene`,\n parent_image_id: image.image_id || \"\",\n version: Number(image.version || 1) + 1,\n regenerate_count: currentCount + 1,\n prompt_id: image.prompt_id || promptJson.prompt_id || \"regenerate_prompt\",\n prompt_text: prompt,\n variant_tag: image.variant_tag || promptJson.variant_tag || \"重生\",\n url: `/generated/${filename}`,\n scene_url: `/generated/${sceneFilename}`,\n prompt_summary: prompt.split(\"\\n\").slice(0, 5).join(\" \"),\n prompt_source: \"S7 edited original prompt\",\n reference_image_count: referenceImages.length,\n reference_image_sources: referencePack.sources,\n reference_image_errors: referencePack.errors,\n generation_mode: generationMode,\n scene_generation_mode: \"scene_template_composite\",\n };\n}\n\nasync function exportIcons(input) {\n const selectedIds = Array.isArray(input.selectedImageIds) ? input.selectedImageIds : [];\n const images = (input.generatedImages || []).filter((image) => selectedIds.includes(image.image_id));\n if (!images.length) throw new Error(\"请先在 S6 选择至少 1 个 icon。\");\n\n const sizes = (Array.isArray(input.sizes) && input.sizes.length ? input.sizes : [1024, 512, 256, 128, 64])\n .map((size) => Number(size))\n .filter((size) => [1024, 512, 256, 128, 64].includes(size));\n const productName = sanitizeFileName(input.product || input.googlePlayProfile?.app_title || \"product\");\n const packageId = `${productName}_icon_delivery_${Date.now()}`;\n const packageDir = path.join(exportDir, packageId);\n fs.mkdirSync(packageDir, { recursive: true });\n\n const files = [];\n for (const [imageIndex, image] of images.entries()) {\n const sourcePath = generatedPathFromUrl(image.url);\n if (!sourcePath || !fs.existsSync(sourcePath)) throw new Error(`找不到已生成 icon 文件:${image.image_id}`);\n const version = image.version || 1;\n const suffix = images.length > 1 ? `_choice${imageIndex + 1}` : \"\";\n for (const size of sizes) {\n const filename = `${productName}_icon${suffix}_v${version}_${size}px.png`;\n const outputPath = path.join(packageDir, filename);\n await sharp(sourcePath)\n .resize(size, size, { fit: \"cover\" })\n .png()\n .toFile(outputPath);\n files.push({\n image_id: image.image_id,\n size,\n filename,\n url: `/exports/${packageId}/${filename}`,\n });\n }\n\n }\n\n const zipName = `${packageId}.zip`;\n const zipPath = path.join(exportDir, zipName);\n await zipDirectory(packageDir, zipPath);\n\n return {\n package_id: packageId,\n zip_url: `/exports/${zipName}`,\n files,\n };\n}\n\nfunction generatedPathFromUrl(value = \"\") {\n if (!value || !value.startsWith(\"/generated/\")) return \"\";\n const filename = path.basename(value);\n return path.join(outputDir, filename);\n}\n\nfunction sanitizeFileName(value = \"\") {\n return String(value || \"product\")\n .trim()\n .replace(/\\s+/g, \"_\")\n .replace(/[^\\w\\u4e00-\\u9fa5-]/g, \"\")\n .toLowerCase() || \"product\";\n}\n\nfunction zipDirectory(sourceDir, zipPath) {\n const zip = new AdmZip();\n zip.addLocalFolder(sourceDir);\n zip.writeZip(zipPath);\n}\n\nasync function makeIconTransparentPng(buffer) {\n const image = sharp(buffer).ensureAlpha();\n const metadata = await image.metadata();\n const width = metadata.width || 0;\n const height = metadata.height || 0;\n if (!width || !height) return image.png().toBuffer();\n\n const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });\n const cornerSize = Math.max(4, Math.min(32, Math.floor(Math.min(info.width, info.height) * 0.035)));\n const samples = [];\n const addSample = (x, y) => {\n const offset = (y * info.width + x) * 4;\n samples.push([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]);\n };\n\n for (let y = 0; y < cornerSize; y += 1) {\n for (let x = 0; x < cornerSize; x += 1) {\n addSample(x, y);\n addSample(info.width - 1 - x, y);\n addSample(x, info.height - 1 - y);\n addSample(info.width - 1 - x, info.height - 1 - y);\n }\n }\n\n const avg = samples.reduce(\n (acc, sample) => {\n acc[0] += sample[0];\n acc[1] += sample[1];\n acc[2] += sample[2];\n acc[3] += sample[3];\n return acc;\n },\n [0, 0, 0, 0],\n ).map((value) => value / samples.length);\n\n if (avg[3] < 245) {\n return sharp(data, { raw: info }).png().toBuffer();\n }\n\n const threshold = 58;\n const visited = new Uint8Array(info.width * info.height);\n const queue = [];\n const isBackgroundLike = (x, y) => {\n const offset = (y * info.width + x) * 4;\n const distance = Math.hypot(data[offset] - avg[0], data[offset + 1] - avg[1], data[offset + 2] - avg[2]);\n return data[offset + 3] > 0 && distance <= threshold;\n };\n const enqueue = (x, y) => {\n if (x < 0 || y < 0 || x >= info.width || y >= info.height) return;\n const key = y * info.width + x;\n if (visited[key] || !isBackgroundLike(x, y)) return;\n visited[key] = 1;\n queue.push([x, y]);\n };\n\n for (let x = 0; x < info.width; x += 1) {\n enqueue(x, 0);\n enqueue(x, info.height - 1);\n }\n for (let y = 0; y < info.height; y += 1) {\n enqueue(0, y);\n enqueue(info.width - 1, y);\n }\n\n for (let index = 0; index < queue.length; index += 1) {\n const [x, y] = queue[index];\n data[(y * info.width + x) * 4 + 3] = 0;\n enqueue(x + 1, y);\n enqueue(x - 1, y);\n enqueue(x, y + 1);\n enqueue(x, y - 1);\n }\n\n return sharp(data, { raw: info }).png().toBuffer();\n}\n\nfunction providerHasKey(provider) {\n if (provider === \"volcengine\") return Boolean(arkApiKey);\n if (provider === \"aliyun\") return Boolean(dashscopeApiKey);\n if (provider === \"kimi\") return Boolean(kimiApiKey);\n if (provider === \"deepseek\") return Boolean(deepseekApiKey);\n return Boolean(apiKey);\n}\n\nasync function generateImage(prompt, referenceImages = []) {\n if (imageProvider === \"volcengine\") {\n const payload = {\n model: volcengineImageModel,\n prompt,\n size: volcengineImageSize,\n response_format: \"url\",\n };\n if (referenceImages.length) {\n payload.image = referenceImages[0];\n payload.images = referenceImages;\n payload.reference_images = referenceImages;\n payload.image_urls = referenceImages;\n }\n const result = await arkFetch(\"images/generations\", payload);\n return extractGeneratedImage(result);\n }\n\n if (imageProvider === \"aliyun\") {\n const task = await dashscopeFetch(\n \"services/aigc/image-generation/generation\",\n {\n model: aliyunImageModel,\n input: {\n messages: [\n {\n role: \"user\",\n content: [{ text: prompt }],\n },\n ],\n },\n parameters: {\n size: aliyunImageSize,\n n: 1,\n },\n },\n { async: true },\n );\n\n const taskId = task.output?.task_id;\n if (!taskId) {\n throw new Error(`DashScope image task did not return task_id: ${JSON.stringify(task)}`);\n }\n\n const result = await pollDashScopeTask(taskId);\n return extractGeneratedImage(result);\n }\n\n if (imageProvider === \"kimi\") {\n const result = await kimiFetchAbsolute(kimiImageEndpoint, {\n model: kimiImageModel,\n prompt,\n size: \"1024x1024\",\n quality: \"medium\",\n response_format: \"b64_json\",\n output_format: \"png\",\n });\n return extractGeneratedImage(result);\n }\n\n const result = await openaiFetch(\"images/generations\", {\n model: imageModel,\n prompt,\n size: \"1024x1024\",\n quality: \"medium\",\n output_format: \"png\",\n });\n return extractGeneratedImage(result);\n}\n\nfunction buildSceneTemplateSvg({ iconBuffer, productName, companyName, platform, variant }) {\n const iconData = `data:image/png;base64,${iconBuffer.toString(\"base64\")}`;\n const title = escapeXml(productName || \"游戏名称\");\n const company = escapeXml(companyName || \"Your Company\");\n const platformName = escapeXml(platform || \"Google Ads\");\n const variantName = escapeXml(variant || \"点击强化\");\n return Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1600\" height=\"1000\" viewBox=\"0 0 1600 1000\">\n <defs>\n <linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#071320\"/>\n <stop offset=\"58%\" stop-color=\"#101b28\"/>\n <stop offset=\"100%\" stop-color=\"#050910\"/>\n </linearGradient>\n <linearGradient id=\"panel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#1d2a38\"/>\n <stop offset=\"100%\" stop-color=\"#0d141e\"/>\n </linearGradient>\n <filter id=\"shadow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\">\n <feDropShadow dx=\"0\" dy=\"18\" stdDeviation=\"16\" flood-color=\"#000\" flood-opacity=\".38\"/>\n </filter>\n <clipPath id=\"r40\"><rect width=\"220\" height=\"220\" rx=\"44\"/></clipPath>\n <clipPath id=\"r24\"><rect width=\"96\" height=\"96\" rx=\"22\"/></clipPath>\n <style>\n .title{font:800 38px Arial,\"Microsoft YaHei\",sans-serif;fill:#fff}\n .sub{font:400 20px Arial,\"Microsoft YaHei\",sans-serif;fill:#d7e3f2}\n .panelTitle{font:800 25px Arial,\"Microsoft YaHei\",sans-serif;fill:#fff}\n .small{font:600 15px Arial,\"Microsoft YaHei\",sans-serif;fill:#d8e1ec}\n .tiny{font:500 13px Arial,\"Microsoft YaHei\",sans-serif;fill:#aeb9c7}\n .darkCard{fill:url(#panel);stroke:#34465a;stroke-width:2}\n .whiteCard{fill:#fff}\n .muted{fill:#f5f7fb}\n .green{fill:#0aa36d}\n </style>\n </defs>\n <rect width=\"1600\" height=\"1000\" fill=\"url(#bg)\"/>\n <text x=\"800\" y=\"48\" text-anchor=\"middle\" class=\"title\">ICON 使用场景展示图模板(通用版)</text>\n <text x=\"800\" y=\"82\" text-anchor=\"middle\" class=\"sub\">适用于 ${platformName} / Meta / TikTok 等主流投放平台 · ${variantName}</text>\n\n <rect x=\"20\" y=\"110\" width=\"450\" height=\"850\" rx=\"18\" class=\"darkCard\"/>\n <text x=\"245\" y=\"145\" text-anchor=\"middle\" class=\"panelTitle\">1. 手机桌面效果</text>\n <text x=\"245\" y=\"172\" text-anchor=\"middle\" class=\"small\">展示 Icon 在手机桌面上的实际显示效果</text>\n <rect x=\"48\" y=\"200\" width=\"250\" height=\"620\" rx=\"34\" fill=\"#0a1220\" stroke=\"#46566a\" stroke-width=\"3\" filter=\"url(#shadow)\"/>\n <text x=\"78\" y=\"235\" class=\"small\">9:41</text>\n ${phoneIconGrid(iconData)}\n <rect x=\"78\" y=\"735\" width=\"190\" height=\"64\" rx=\"24\" fill=\"#223247\" opacity=\".86\"/>\n ${dockIcon(96, 753, \"#39d353\")}\n ${dockIcon(142, 753, \"#37a0ff\")}\n ${dockIcon(188, 753, \"#49d16d\")}\n ${dockIcon(234, 753, \"#ff4d55\")}\n <text x=\"50\" y=\"875\" class=\"small\">展示要点:</text>\n <text x=\"50\" y=\"905\" class=\"tiny\">• 不同系统桌面下的清晰度与吸引力</text>\n <text x=\"50\" y=\"930\" class=\"tiny\">• 小尺寸下的主体轮廓和识别度</text>\n\n <rect x=\"485\" y=\"110\" width=\"470\" height=\"850\" rx=\"18\" class=\"darkCard\"/>\n <text x=\"720\" y=\"145\" text-anchor=\"middle\" class=\"panelTitle\">2. Google Play 详情页效果</text>\n <text x=\"720\" y=\"172\" text-anchor=\"middle\" class=\"small\">展示 Icon 在商店详情页中的呈现效果</text>\n <rect x=\"510\" y=\"200\" width=\"420\" height=\"620\" rx=\"16\" class=\"whiteCard\" filter=\"url(#shadow)\"/>\n <text x=\"555\" y=\"242\" font-family=\"Arial\" font-size=\"24\" fill=\"#303846\">Google Play</text>\n <image href=\"${iconData}\" x=\"548\" y=\"290\" width=\"112\" height=\"112\"/>\n <text x=\"690\" y=\"320\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"30\" font-weight=\"800\" fill=\"#101828\">${title}</text>\n <text x=\"690\" y=\"354\" font-family=\"Arial\" font-size=\"18\" font-weight=\"700\" fill=\"#0a8f61\">${company}</text>\n <text x=\"690\" y=\"388\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"18\" fill=\"#344054\">解谜 · 冒险 · 探索</text>\n <text x=\"555\" y=\"455\" font-family=\"Arial\" font-size=\"22\" fill=\"#101828\">4.6★</text>\n <text x=\"680\" y=\"455\" font-family=\"Arial\" font-size=\"22\" fill=\"#101828\">1000万+</text>\n <text x=\"820\" y=\"455\" font-family=\"Arial\" font-size=\"22\" fill=\"#101828\">12+</text>\n <rect x=\"548\" y=\"505\" width=\"340\" height=\"52\" rx=\"8\" fill=\"#0aa36d\"/>\n <text x=\"718\" y=\"539\" text-anchor=\"middle\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"20\" font-weight=\"800\" fill=\"#fff\">安装</text>\n ${storeShotRow(iconData)}\n <text x=\"530\" y=\"875\" class=\"small\">展示要点:</text>\n <text x=\"530\" y=\"905\" class=\"tiny\">• 商店详情页中的清晰度与吸引力</text>\n <text x=\"530\" y=\"930\" class=\"tiny\">• 与应用名称、分类、评分的搭配效果</text>\n\n <rect x=\"970\" y=\"110\" width=\"610\" height=\"850\" rx=\"18\" class=\"darkCard\"/>\n <text x=\"1275\" y=\"145\" text-anchor=\"middle\" class=\"panelTitle\">3. 广告平台主要广告展示效果</text>\n <text x=\"1275\" y=\"172\" text-anchor=\"middle\" class=\"small\">展示 Icon 在不同广告位中的实际呈现效果</text>\n ${adCards(iconData, title, company)}\n <text x=\"995\" y=\"875\" class=\"small\">展示要点:</text>\n <text x=\"995\" y=\"905\" class=\"tiny\">• 不同广告位和尺寸下的表现</text>\n <text x=\"995\" y=\"930\" class=\"tiny\">• 与文案、素材的搭配协调性</text>\n <text x=\"1240\" y=\"905\" class=\"tiny\">• 吸引用户点击的视觉表现</text>\n <text x=\"800\" y=\"984\" text-anchor=\"middle\" class=\"tiny\">注:场景展示图由生成 Icon 原样嵌入模板,仅用于投放预览。</text>\n</svg>`);\n}\n\nfunction phoneIconGrid(iconData) {\n const colors = [\"#ffffff\", \"#f2b84b\", \"#5aa7ff\", \"#d7dde7\", \"#46c970\", \"#3182ff\", \"#fff\", \"#a7b2c3\"];\n const cells = [];\n for (let row = 0; row < 4; row += 1) {\n for (let col = 0; col < 4; col += 1) {\n const x = 75 + col * 52;\n const y = 275 + row * 84;\n if (row === 2 && col === 0) {\n cells.push(`<image href=\"${iconData}\" x=\"${x}\" y=\"${y}\" width=\"42\" height=\"42\"/><text x=\"${x + 21}\" y=\"${y + 62}\" text-anchor=\"middle\" class=\"tiny\">游戏</text>`);\n } else {\n cells.push(`<rect x=\"${x}\" y=\"${y}\" width=\"42\" height=\"42\" rx=\"10\" fill=\"${colors[(row * 4 + col) % colors.length]}\"/><text x=\"${x + 21}\" y=\"${y + 62}\" text-anchor=\"middle\" class=\"tiny\">App</text>`);\n }\n }\n }\n return cells.join(\"\");\n}\n\nfunction dockIcon(x, y, color) {\n return `<rect x=\"${x}\" y=\"${y}\" width=\"34\" height=\"34\" rx=\"9\" fill=\"${color}\"/>`;\n}\n\nfunction storeShotRow(iconData) {\n return [0, 1, 2, 3]\n .map((i) => `<rect x=\"${548 + i * 86}\" y=\"600\" width=\"72\" height=\"150\" rx=\"10\" fill=\"#111827\"/><image href=\"${iconData}\" x=\"${560 + i * 86}\" y=\"635\" width=\"48\" height=\"48\"/>`)\n .join(\"\");\n}\n\nfunction adCards(iconData, title, company) {\n return `\n <rect x=\"995\" y=\"210\" width=\"300\" height=\"160\" rx=\"12\" fill=\"#f8fafc\"/>\n <image href=\"${iconData}\" x=\"1020\" y=\"245\" width=\"64\" height=\"64\"/>\n <text x=\"1100\" y=\"260\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"20\" font-weight=\"800\" fill=\"#111827\">${title}</text>\n <text x=\"1100\" y=\"286\" font-family=\"Arial\" font-size=\"14\" fill=\"#475467\">${company}</text>\n <text x=\"1100\" y=\"318\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"16\" fill=\"#111827\">探索神秘旅程!</text>\n <rect x=\"1218\" y=\"320\" width=\"54\" height=\"28\" rx=\"7\" fill=\"#2f6bff\"/><text x=\"1245\" y=\"339\" text-anchor=\"middle\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"14\" font-weight=\"800\" fill=\"#fff\">安装</text>\n\n <rect x=\"995\" y=\"395\" width=\"300\" height=\"245\" rx=\"12\" fill=\"#f8fafc\"/>\n <image href=\"${iconData}\" x=\"1020\" y=\"425\" width=\"42\" height=\"42\"/>\n <text x=\"1075\" y=\"442\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"18\" font-weight=\"800\" fill=\"#111827\">${title}</text>\n <rect x=\"1020\" y=\"480\" width=\"250\" height=\"110\" rx=\"10\" fill=\"#0d1724\"/>\n <image href=\"${iconData}\" x=\"1110\" y=\"500\" width=\"70\" height=\"70\"/>\n <text x=\"1020\" y=\"622\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"14\" fill=\"#111827\">立即下载,开启冒险!</text>\n\n <rect x=\"1320\" y=\"210\" width=\"210\" height=\"430\" rx=\"18\" fill=\"#080e17\"/>\n <rect x=\"1345\" y=\"240\" width=\"160\" height=\"260\" rx=\"10\" fill=\"#122033\"/>\n <image href=\"${iconData}\" x=\"1378\" y=\"310\" width=\"96\" height=\"96\"/>\n <text x=\"1360\" y=\"535\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"18\" font-weight=\"800\" fill=\"#fff\">${title}</text>\n <text x=\"1360\" y=\"562\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"14\" fill=\"#d7e3f2\">探索隐藏奖励!</text>\n <rect x=\"1360\" y=\"590\" width=\"120\" height=\"34\" rx=\"7\" fill=\"#ff2f55\"/><text x=\"1420\" y=\"613\" text-anchor=\"middle\" font-family=\"Arial,'Microsoft YaHei'\" font-size=\"15\" font-weight=\"800\" fill=\"#fff\">立即下载</text>`;\n}\n\nfunction escapeXml(value = \"\") {\n return String(value)\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nasync function pollDashScopeTask(taskId) {\n const deadline = Date.now() + 1000 * 60 * 6;\n let lastResult = null;\n\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 2500));\n lastResult = await dashscopeGet(`tasks/${taskId}`);\n const status = lastResult.output?.task_status || lastResult.output?.status;\n if (status === \"SUCCEEDED\") return lastResult;\n if (status === \"FAILED\" || status === \"UNKNOWN\") {\n throw new Error(lastResult.output?.message || `DashScope image task ${status}`);\n }\n }\n\n throw new Error(`DashScope image task timed out: ${taskId}. Last result: ${JSON.stringify(lastResult)}`);\n}\n\nfunction extractGeneratedImage(result) {\n const image =\n result.data?.[0] ||\n result.images?.[0] ||\n result.output?.[0] ||\n result.output?.results?.[0] ||\n result.output?.task_results?.[0] ||\n result.output?.choices?.[0]?.message?.content?.find((part) => part.image || part.image_url || part.url);\n if (image?.b64_json || image?.url || image?.image || image?.image_url) return image;\n\n const content = result.choices?.[0]?.message?.content;\n if (Array.isArray(content)) {\n const item = content.find((part) => part.image_url?.url || part.url || part.b64_json);\n if (item) return item.image_url || item;\n }\n\n const url = result.output?.url || result.output?.image_url || result.url || result.image_url || result.image;\n if (url) return { url: typeof url === \"string\" ? url : url.url };\n\n throw new Error(\"Image API did not return b64_json, url, or image field. Check image provider response format.\");\n}\n\nasync function resolveImageBuffer(image) {\n if (image.b64_json) {\n return Buffer.from(image.b64_json, \"base64\");\n }\n\n const url = image.url || image.image || image.image_url?.url || image.image_url;\n if (!url) {\n throw new Error(\"Image API result has no b64_json or url.\");\n }\n\n if (url.startsWith(\"data:image/\")) {\n return Buffer.from(url.split(\",\")[1] || \"\", \"base64\");\n }\n\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to download generated image: HTTP ${response.status}`);\n }\n return Buffer.from(await response.arrayBuffer());\n}\n\nasync function qaImages(input) {\n const images = input.generatedImages || [];\n const reports = [];\n\n for (const image of images) {\n const filename = path.basename(image.url || \"\");\n const filePath = path.join(outputDir, filename);\n if (!fs.existsSync(filePath)) continue;\n\n const b64 = fs.readFileSync(filePath).toString(\"base64\");\n const text = await runStructuredAnalysis({\n text: `请作为广告投放 icon 质检员,对这张 icon 做自动质检,只返回 JSON。\n检查项:\n- must_include 是否体现产品主体:${input.product || \"未填写\"}\n- must_not_include 是否避开:${input.sensitive || \"误导承诺、血腥、裸露、侵权元素\"}\n- 64px 小尺寸识别度\n- 单主体、中心构图、高对比、简单背景\n- 平台审核风险:${input.platform || \"Google Ads\"}\n输出字段:image_id, passed, score_0_100, issues[], recommendation。`,\n images: [\n {\n type: \"input_image\",\n image_url: `data:image/png;base64,${b64}`,\n detail: \"low\",\n },\n ],\n });\n\n const parsed = parseJsonLoose(text);\n reports.push(parsed ? { image_id: image.image_id, ...parsed } : { image_id: image.image_id, passed: false, raw_text: text });\n }\n\n return reports;\n}\n\nfunction serveStatic(req, res) {\n const url = new URL(req.url, `http://localhost:${port}`);\n const pathname = decodeURIComponent(url.pathname);\n const relativePath = pathname === \"/\" ? \"index.html\" : pathname.replace(/^\\/+/, \"\");\n const filePath = path.normalize(path.join(rootDir, relativePath));\n\n if (!filePath.startsWith(rootDir)) {\n res.writeHead(403, corsHeaders());\n res.end(\"Forbidden\");\n return;\n }\n\n fs.readFile(filePath, (error, data) => {\n if (error) {\n res.writeHead(404, corsHeaders());\n res.end(\"Not found\");\n return;\n }\n\n const ext = path.extname(filePath).toLowerCase();\n res.writeHead(200, {\n ...corsHeaders(),\n \"Content-Type\": mimeTypes[ext] || \"application/octet-stream\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(data);\n });\n}\n\nconst server = http.createServer(async (req, res) => {\n try {\n const url = new URL(req.url, `http://localhost:${port}`);\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders());\n res.end();\n return;\n }\n\n if (url.pathname === \"/api/health\") {\n sendJson(res, 200, {\n ok: true,\n ai_provider: aiProvider,\n image_provider: imageProvider,\n has_api_key: providerHasKey(aiProvider) && providerHasKey(imageProvider),\n has_kimi_api_key: Boolean(kimiApiKey),\n has_deepseek_api_key: Boolean(deepseekApiKey),\n has_dashscope_api_key: Boolean(dashscopeApiKey),\n has_ark_api_key: Boolean(arkApiKey),\n text_model: textModel,\n kimi_text_model: kimiTextModel,\n deepseek_text_model: deepseekTextModel,\n aliyun_text_model: aliyunTextModel,\n volcengine_text_model: volcengineTextModel,\n image_model: imageModel,\n kimi_image_model: kimiImageModel,\n kimi_image_endpoint: kimiImageEndpoint,\n aliyun_image_model: aliyunImageModel,\n aliyun_image_size: aliyunImageSize,\n volcengine_image_model: volcengineImageModel,\n volcengine_image_size: volcengineImageSize,\n ark_base_url: arkBaseUrl,\n });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/analyze\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, analysis: await analyzeWorkflow(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/optimize-prompts\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, prompt_plan: await optimizePromptsConfigured(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/check-prompts\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, report: await checkPrompts(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/google-play\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, google_play: await googlePlayLookup(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/upload-reference\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, file: await uploadReferenceImage(body, req) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/generate-icons\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, images: await generateIcons(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/regenerate-icon\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, image: await regenerateIcon(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/export-icons\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, export: await exportIcons(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/qa\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, reports: await qaImages(body) });\n return;\n }\n\n if (req.method === \"POST\" && url.pathname === \"/api/smoke-test\") {\n const body = await readBody(req);\n sendJson(res, 200, { ok: true, result: await runSmokeTest(String(body.mode || \"quick\"), req) });\n return;\n }\n\n serveStatic(req, res);\n } catch (error) {\n sendJson(res, 500, { ok: false, error: error.message });\n }\n});\n\nserver.listen(port, () => {\n console.log(`应用icon创作大师已启动:http://localhost:${port}`);\n console.log(`模型状态:${apiKey ? \"已读取 OPENAI_API_KEY\" : \"未设置 OPENAI_API_KEY\"}`);\n});\n"}}}}