├── { ├── .env ├── LICENSE ├── README.md ├── index.html └── server.js /{: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | COOKIE= 2 | i18next= 3 | DEVICE_ID= 4 | TEA_UUID= 5 | WEB_ID= 6 | MS_TOKEN= 7 | A_BOGUS= 8 | ROOM_ID= 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 giaoimgiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍥 Doubao Image Proxy 2 | 3 | > **A minimal self‑hosted Node.js service that turns Doubao AI’s *mysterious* SSE image stream into ordinary PNG/URL you can save or embed.** 4 | > 完整抓包 + SSE 解析 + 跨格式转码,一次跑通! 5 | 6 | --- 7 | 8 | ## ✨ Features 9 | 10 | | 功能 | 说明 | 11 | | ------------------------- | ---------------------------------------------------------------------- | 12 | | 🎨 **Prompt → URL / PNG** | 提示词一键生成图片 URL,或本地 `pic.png` | 13 | | ♻️ **SSE 解析** | 全量解析 `event: 2001/2003` 字段,自动捕获 `content_type 2074` 中的 `status = 2` 图链 | 14 | | 🛟 **轮询兜底** | 若 SSE 漏图,自动用 `node_id` 走官方 *message\_node\_info* 轮询接口重试 | 15 | | 🪄 **格式兼容** | 下载任意 `webp / avif / jpeg / png` 并用 **sharp** 转为 PNG | 16 | | 🖥️ **前端零依赖** | 自带超简 `index.html`,浏览器直接输入提示词即可 | 17 | 18 | --- 19 | 20 | ## 🚀 Quick Start 21 | 22 | ```bash 23 | # 1. clone & install 24 | npm i 25 | 26 | # 2. 准备 .env(👇见下一节) 27 | cp .env.example .env # 然后填值 28 | 29 | # 3. run 30 | node server.js 31 | # => http://localhost:3000 ✨ 32 | ``` 33 | 34 | 打开浏览器 → 输入提示词 → 等待控制台打印 `✅ 找到图片真实URL` → 成功! 35 | 36 | --- 37 | 38 | ## ⚙️ Required .env 39 | 40 | ```dotenv 41 | COOKIE=xxx # 全站 Cookie 字符串 42 | X_MS_TOKEN=xxx # 请求头 x-ms-token(SSE 必带) 43 | DEVICE_ID=7507... # 抓包得来的 device_id 44 | TEA_UUID=7507... # tea_uuid 同上 45 | WEB_ID=7507... # web_id 46 | MS_TOKEN=lrmtE... # querystring msToken 47 | A_BOGUS=YfU5g... # querystring a_bogus 48 | ROOM_ID=7338... # 浏览器 chat/{ROOM_ID} 里的数字 49 | PORT=3000 # 可选,默认 3000 50 | ``` 51 | 52 | > **怎么抓?** 打开 `doubao.com`, F12 ➜ Network ➜ 过滤 `completion?` 请求,复制 *Request Headers* / *cookie* / URL 参数即可。 53 | 54 | --- 55 | 56 | ## 🧩 Project Structure 57 | 58 | ``` 59 | . 60 | ├── server.js # 核心代理 61 | ├── index.html # 超简前端 62 | ├── /public 63 | │ └── pic.png # 最新生成的 PNG 64 | └── .env # 私有令牌 65 | ``` 66 | 67 | --- 68 | 69 | ## 🛠️ How It Works 70 | 71 | 1. **POST /generate** 接收提示词 → 转成官方聊天接口 payload 72 | 2. **SSE 流**: 73 | 74 | * 监听 `event_type 2001`,定位 `content_type 2074` 消息 75 | * 解析 `creations[]`,筛 `image.status === 2` 76 | 3. 若 SSE 漏图 → **轮询** `/message_node_info` 直到拿到 `status 2` 77 | 4. **下载** `image_ori ⇢ image_raw ⇢ thumb`(按优先级) 78 | 5. **sharp** 转 PNG → `public/pic.png` 79 | 6. 响应 `{ url: "/pic.png", urls: [..] }` 80 | 81 | --- 82 | 83 | ## 🐛 Troubleshooting 84 | 85 | | 现象 | 解决 | 86 | | ----------------------------- | ------------------------------------------- | 87 | | 控制台刷 `type=2001` 无 `status=2` | 确认 **COOKIE / TOKEN** 未过期;必要时清理 `.env` 重新抓包 | 88 | | 轮询 5 次仍失败 | Doubao 后端卡住了… 重发提示词或降级提示词内容 | 89 | | `sharp` 报格式不支持 | 自动 fallback 写原图;请升级 sharp 或安装系统依赖 | 90 | 91 | --- 92 | 93 | ## 📜 License 94 | 95 | [MIT](LICENSE) – 不喜请自便。 96 | 97 | > *Made with coffee ☕ + infinite patience.* 98 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 图片生成示例 6 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 生成的图片将在此显示 52 | 53 | 54 | 127 | 128 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 极简图片生成代理服务 3 | * 4 | * 使用说明: 5 | * 1. 安装依赖: npm i express sharp # 若 Node <18 需额外安装 node-fetch@^3 6 | * 2. 替换CONFIG对象中的硬编码值(从浏览器抓包获取) 7 | * 3. 运行: node server.js 8 | */ 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const express = require('express'); 12 | const sharp = require('sharp'); 13 | const dotenv = require('dotenv'); 14 | 15 | 16 | dotenv.config(); 17 | const CONFIG = process.env; // ← 用环境变量 18 | 19 | // 检查必填 20 | ['COOKIE','X_MS_TOKEN','DEVICE_ID','TEA_UUID','WEB_ID','MS_TOKEN','A_BOGUS','ROOM_ID'] 21 | .forEach(k => { if (!CONFIG[k]) { console.error(`缺少 ${k}`); process.exit(1);} }); 22 | 23 | const BASE_URL = 'https://www.doubao.com'; 24 | 25 | // Node 18+ 自带 fetch;若当前环境无,则动态加载 node-fetch@3 26 | let fetch = global.fetch; 27 | if (!fetch) { 28 | const nodeFetch = require('node-fetch'); 29 | fetch = nodeFetch; 30 | } 31 | 32 | const app = express(); 33 | const PORT = process.env.PORT || 3000; 34 | 35 | // 确保 public 目录存在,用于存放 pic.png 36 | const publicDir = path.join(__dirname, 'public'); 37 | if (!fs.existsSync(publicDir)) fs.mkdirSync(publicDir); 38 | 39 | // 静态托管根目录(index.html 等)与 public 目录(生成图片) 40 | app.use(express.static(__dirname)); 41 | app.use(express.static(publicDir)); 42 | app.use(express.json()); 43 | 44 | // 基础URL和必备参数 45 | const getQueryParams = () => { 46 | return `aid=497858&device_id=${CONFIG.DEVICE_ID}&device_platform=web&language=zh&pc_version=2.16.7&pkg_type=release_version&real_aid=497858®ion=CN&samantha_web=1&sys_region=CN&tea_uuid=${CONFIG.TEA_UUID}&use-olympus-account=1&version_code=20800&web_id=${CONFIG.WEB_ID}&msToken=${CONFIG.MS_TOKEN}&a_bogus=${CONFIG.A_BOGUS}`; 47 | }; 48 | 49 | // 通用请求头 50 | const getHeaders = () => { 51 | return { 52 | 'content-type': 'application/json', 53 | 'accept': 'text/event-stream', 54 | 'agw-js-conv': 'str', 55 | 'cookie': CONFIG.COOKIE, 56 | 'x-ms-token': CONFIG.X_MS_TOKEN, 57 | 'origin': BASE_URL, 58 | 'referer': `${BASE_URL}/chat/${CONFIG.ROOM_ID}`, 59 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36' 60 | }; 61 | }; 62 | 63 | app.post('/generate', async (req, res) => { 64 | try { 65 | const prompt = (req.body && req.body.text) || ''; 66 | if (!prompt) return res.status(400).json({ error: '提示词不能为空' }); 67 | 68 | console.log(`正在生成图片,提示词: "${prompt}"`); 69 | console.log(`请求头: ${JSON.stringify(req.headers)}`); 70 | console.log(`请求体: ${JSON.stringify(req.body)}`); 71 | 72 | // 1. 调用生成接口,处理SSE流 73 | const result = await generateImage(prompt); 74 | 75 | if (!result.imageUrl) { 76 | return res.status(500).json({ error: '未能获取到图片URL' }); 77 | } 78 | 79 | // 如果有多张图片URL,直接返回给前端 80 | if (result.allImageUrls && result.allImageUrls.length > 0) { 81 | console.log(`找到 ${result.allImageUrls.length} 张图片,直接返回URL给前端`); 82 | return res.json({ 83 | urls: result.allImageUrls, 84 | url: result.imageUrl // 兼容旧版 85 | }); 86 | } 87 | 88 | // 2. 下载图片并转换 89 | const pngPath = await downloadAndConvertImage(result.imageUrl); 90 | 91 | // 3. 返回本地图片路径 92 | res.json({ url: '/pic.png' }); 93 | } catch (err) { 94 | console.error('图片生成失败:', err); 95 | console.error('错误详情:', err.stack); 96 | console.error('错误类型:', err.name); 97 | console.error('错误消息:', err.message); 98 | 99 | // 返回更详细的错误信息给客户端 100 | res.status(500).json({ 101 | error: err.message || '服务器错误', 102 | errorType: err.name, 103 | errorStack: process.env.NODE_ENV !== 'production' ? err.stack : undefined 104 | }); 105 | } 106 | }); 107 | 108 | // 生成图片,解析SSE流 109 | async function generateImage(promptText) { 110 | const url = `${BASE_URL}/samantha/chat/completion?${getQueryParams()}`; 111 | 112 | // 生成UUID作为本地消息ID 113 | const generateUUID = () => { 114 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 115 | const r = Math.random() * 16 | 0; 116 | const v = c === 'x' ? r : (r & 0x3 | 0x8); 117 | return v.toString(16); 118 | }); 119 | }; 120 | 121 | const localMsgId = generateUUID(); 122 | const localConvId = `local_${Math.floor(Math.random() * 10000000000000000)}`; 123 | 124 | // 新的API请求格式 125 | const body = { 126 | completion_option: { 127 | is_regen: false, 128 | with_suggest: true, 129 | need_create_conversation: true, 130 | launch_stage: 1, 131 | reply_id: "0" 132 | }, 133 | conversation_id: "0", 134 | local_conversation_id: localConvId, 135 | local_message_id: localMsgId, 136 | messages: [ 137 | { 138 | content: JSON.stringify({text: promptText}), 139 | content_type: 2001, 140 | attachments: [], 141 | references: [] 142 | } 143 | ] 144 | }; 145 | 146 | console.log('发送请求到生成接口...'); 147 | console.log(`请求URL: ${url}`); 148 | console.log(`请求体: ${JSON.stringify(body)}`); 149 | 150 | try { 151 | const controller = new AbortController(); 152 | const response = await fetch(url, { 153 | method: 'POST', 154 | headers: getHeaders(), 155 | body: JSON.stringify(body), 156 | signal: controller.signal 157 | }); 158 | 159 | console.log(`响应状态: ${response.status} ${response.statusText}`); 160 | console.log(`响应头: ${JSON.stringify([...response.headers.entries()])}`); 161 | 162 | if (!response.ok) { 163 | const errorText = await response.text(); 164 | console.error(`请求失败详情: ${errorText}`); 165 | throw new Error(`生成请求失败: ${response.status} ${response.statusText}, 详情: ${errorText}`); 166 | } 167 | 168 | if (!response.body) { 169 | throw new Error('生成请求返回异常: 无响应体'); 170 | } 171 | 172 | console.log('开始处理SSE流...'); 173 | 174 | // 全局变量,用于保存最终的图片URL 175 | let imageUrl = null; 176 | let nodeId = null; 177 | // 保存所有找到的图片URL 178 | const allImageUrls = []; 179 | // 保存最终结果,解决abort导致的问题 180 | let finalResult = null; 181 | 182 | // 新的 SSE 处理函数 183 | async function handleSSE(stream, onImage) { 184 | const decoder = new TextDecoder(); 185 | const reader = stream.getReader(); 186 | let buffer = ''; 187 | 188 | while (true) { 189 | const { value, done } = await reader.read(); 190 | if (done) break; 191 | buffer += decoder.decode(value, { stream: true }); 192 | 193 | // 检查是否是网关错误 194 | if (buffer.includes('event: gateway-error')) { 195 | const errorMatch = buffer.match(/data:\s*({.*})/); 196 | if (errorMatch && errorMatch[1]) { 197 | try { 198 | const errorData = JSON.parse(errorMatch[1]); 199 | console.error('服务器网关错误:', errorData); 200 | throw new Error(`服务器返回网关错误: ${errorData.code} - ${errorData.message}`); 201 | } catch (e) { 202 | throw new Error(`服务器返回网关错误: ${buffer}`); 203 | } 204 | } else { 205 | throw new Error(`服务器返回网关错误: ${buffer}`); 206 | } 207 | } 208 | 209 | // 根据 "\n\n" 拆成完整 event;最后一个可能是半截,留给下轮补全 210 | const events = buffer.split('\n\n'); 211 | buffer = events.pop(); // 保留最后可能不完整的部分 212 | 213 | for (const evt of events) { 214 | const line = evt.trim().split('\n') // 每行 "event: xxx" / "data: xxx" 215 | .find(l => l.startsWith('data: ')); 216 | if (!line) continue; 217 | 218 | try { 219 | const evtObj = JSON.parse(line.slice(6)); // 外层 220 | console.log(`解析到事件: type=${evtObj.event_type || 'unknown'}`); 221 | 222 | // 保存node_id备用 223 | if (evtObj.event_type === 2001) { 224 | try { 225 | const inner = JSON.parse(evtObj.event_data); // event_data 226 | if (!nodeId && inner.node_id) { 227 | nodeId = inner.node_id; 228 | console.log('保存node_id:', nodeId); 229 | } 230 | 231 | const msg = inner.message; 232 | if (!nodeId && msg?.id) { 233 | nodeId = msg.id; 234 | console.log('保存真正的node_id (message.id):', nodeId); 235 | } 236 | 237 | // 只处理图片消息 238 | if (msg?.content_type === 2074) { 239 | console.log('找到图片载体消息 (content_type 2074)'); 240 | const content = JSON.parse(msg.content); // creations 数组 241 | 242 | console.log(`找到 ${content.creations?.length || 0} 张图片信息,状态:`, 243 | content.creations?.map(c => c.image?.status || 'unknown').join(', ')); 244 | 245 | let foundStatus2 = false; 246 | for (const creation of content.creations || []) { 247 | // 只处理status为2的完成图片 248 | if (creation?.image?.status === 2) { 249 | const url = creation.image.image_ori?.url || 250 | creation.image.image_raw?.url || 251 | creation.image.image_thumb?.url; 252 | 253 | if (url) { 254 | console.log(`✅ 找到图片真实URL (status=2):`, url); 255 | foundStatus2 = true; 256 | onImage(url); 257 | } 258 | } 259 | } 260 | 261 | // 如果找到了完成的图片,可以提前结束 262 | if (foundStatus2) { 263 | console.log('找到有效图片,可以提前结束处理'); 264 | return; 265 | } 266 | } 267 | // 常规进度报告 268 | else if (inner.step) { 269 | console.log(`生成进度: ${Math.round(inner.step * 100)}%`); 270 | } 271 | } catch (e) { 272 | console.error('解析内层事件数据失败:', e); 273 | } 274 | } 275 | // event_type 2003表示流结束 276 | else if (evtObj.event_type === 2003) { 277 | console.log('收到流结束事件 (2003)'); 278 | return; 279 | } 280 | } catch (e) { 281 | console.log('解析事件失败(可能是不完整的JSON):', e.message); 282 | } 283 | } 284 | } 285 | } 286 | 287 | try { 288 | // 使用新的 SSE 处理函数 289 | await handleSSE(response.body, (url) => { 290 | allImageUrls.push(url); 291 | // 只保存第一张作为主图 292 | if (!imageUrl) { 293 | imageUrl = url; 294 | finalResult = { imageUrl, allImageUrls }; 295 | } 296 | }); 297 | 298 | // 如果有最终结果,直接返回 299 | if (finalResult) { 300 | console.log(`✅ 共找到 ${allImageUrls.length} 张有效图片,将使用第一张`); 301 | return finalResult; 302 | } 303 | } catch (err) { 304 | console.error('处理SSE流失败:', err); 305 | if (allImageUrls.length > 0) { 306 | // 如果已有图片URL,仍然可以返回 307 | console.log('尽管出错,但已找到图片URL,继续处理'); 308 | return { imageUrl, allImageUrls }; 309 | } 310 | throw err; 311 | } 312 | 313 | // 若SSE流中没获取到图片,尝试轮询获取 314 | if (!imageUrl && nodeId) { 315 | console.log(`SSE流中未找到图片URL,使用node_id进行轮询: ${nodeId}`); 316 | const result = await pollImageResult(nodeId); 317 | return result; 318 | } 319 | 320 | return { imageUrl, allImageUrls }; // 兼容原逻辑 321 | } catch (err) { 322 | console.error('图片生成请求失败:', err); 323 | throw err; 324 | } 325 | } 326 | 327 | // 轮询获取图片结果 328 | async function pollImageResult(nodeId, maxRetries = 5) { 329 | const url = `${BASE_URL}/samantha/aispace/message_node_info?${getQueryParams()}`; 330 | const body = { node_id: nodeId }; 331 | 332 | console.log('开始轮询图片结果...'); 333 | console.log(`轮询URL: ${url}`); 334 | console.log(`轮询请求体: ${JSON.stringify(body)}`); 335 | 336 | for (let i = 0; i < maxRetries; i++) { 337 | try { 338 | console.log(`轮询尝试 ${i+1}/${maxRetries}...`); 339 | 340 | const response = await fetch(url, { 341 | method: 'POST', 342 | headers: getHeaders(), 343 | body: JSON.stringify(body) 344 | }); 345 | 346 | console.log(`轮询响应状态: ${response.status} ${response.statusText}`); 347 | 348 | if (!response.ok) { 349 | const errorText = await response.text(); 350 | console.error(`轮询失败详情: ${errorText}`); 351 | console.error(`轮询请求失败: ${response.status}, 详情: ${errorText}`); 352 | continue; 353 | } 354 | 355 | const data = await response.json(); 356 | console.log(`轮询响应数据片段: ${JSON.stringify(data).substring(0, 200)}...`); 357 | 358 | if (data.code !== 0) { 359 | console.error(`API返回错误码: ${data.code}, ${data.msg}`); 360 | continue; 361 | } 362 | 363 | // 处理新版API结构 (2074 content_type) 364 | if (data.data && data.data.messages && data.data.messages[0]) { 365 | const msg = data.data.messages[0]; 366 | if (msg.content_type === 2074 && msg.content) { 367 | try { 368 | const payload = JSON.parse(msg.content); 369 | if (payload.creations && payload.creations.length > 0) { 370 | // 寻找status为2的所有图片 371 | const allImageUrls = []; 372 | let imageUrl = null; 373 | 374 | payload.creations.forEach((creation, index) => { 375 | // 只处理status为2的完成图片 376 | if (creation.image && creation.image.status === 2) { 377 | const url = creation.image.image_ori?.url || 378 | creation.image.image_raw?.url || 379 | creation.image.image_thumb?.url; 380 | 381 | if (url) { 382 | console.log(`轮询找到第${index+1}张有效图片URL (status=2): ${url}`); 383 | allImageUrls.push(url); 384 | 385 | // 第一张作为主图 386 | if (!imageUrl) { 387 | imageUrl = url; 388 | } 389 | } 390 | } else if (creation.image) { 391 | console.log(`轮询中图片 ${index+1} 状态为 ${creation.image.status || 'unknown'},跳过...`); 392 | } 393 | }); 394 | 395 | if (allImageUrls.length > 0) { 396 | console.log(`轮询共找到 ${allImageUrls.length} 张有效图片URL`); 397 | return { imageUrl, allImageUrls }; 398 | } 399 | 400 | console.log('轮询到的图片都不是status=2的状态,继续等待...'); 401 | } 402 | } catch (e) { 403 | console.error('解析2074消息内容失败:', e); 404 | } 405 | } 406 | } 407 | 408 | // 检查其他可能的路径获取图片URL (旧版兼容) 409 | let imageUrl = null; 410 | 411 | // 尝试不同的路径获取图片URL 412 | if (data.data.elements && data.data.elements[0] && data.data.elements[0].type === 'image') { 413 | imageUrl = data.data.elements[0].url; 414 | console.log('轮询通过elements路径找到图片URL'); 415 | } else if (data.data.message?.elements && data.data.message.elements[0] && data.data.message.elements[0].type === 'image') { 416 | imageUrl = data.data.message.elements[0].url; 417 | console.log('轮询通过message.elements路径找到图片URL'); 418 | } else if (data.data.messages && data.data.messages[0] && data.data.messages[0].attachments) { 419 | const attach = data.data.messages[0].attachments[0]; 420 | if (attach && attach.type === 'image' && attach.url) { 421 | imageUrl = attach.url; 422 | console.log('轮询通过messages[0].attachments路径找到图片URL'); 423 | } 424 | } 425 | 426 | if (imageUrl) { 427 | return { imageUrl, allImageUrls: [imageUrl] }; 428 | } 429 | 430 | if (data.data.status === 'progress') { 431 | console.log('图片仍在生成中,等待下次轮询...'); 432 | } 433 | 434 | // 等待1.5秒后继续轮询 435 | await new Promise(resolve => setTimeout(resolve, 1500)); 436 | } catch (err) { 437 | console.error('轮询过程出错:', err); 438 | console.error('错误详情:', err.stack); 439 | } 440 | } 441 | 442 | throw new Error('轮询超过最大次数,未能获取图片'); 443 | } 444 | 445 | // 下载图片并转换为PNG 446 | async function downloadAndConvertImage(imageUrl) { 447 | console.log('开始下载图片...'); 448 | console.log(`图片URL: ${imageUrl}`); 449 | 450 | try { 451 | const imgResp = await fetch(imageUrl); 452 | console.log(`图片下载响应状态: ${imgResp.status} ${imgResp.statusText}`); 453 | 454 | if (!imgResp.ok) { 455 | const errorText = await imgResp.text(); 456 | console.error(`图片下载失败详情: ${errorText}`); 457 | throw new Error(`下载图片失败: ${imgResp.status}, 详情: ${errorText}`); 458 | } 459 | 460 | console.log('图片下载完成,准备转换为PNG...'); 461 | 462 | const imgBuffer = Buffer.from(await imgResp.arrayBuffer()); 463 | console.log(`下载的图片大小: ${imgBuffer.length} 字节`); 464 | const pngPath = path.join(publicDir, 'pic.png'); 465 | 466 | // 处理各种格式的图片,都转为PNG 467 | try { 468 | await sharp(imgBuffer).png().toFile(pngPath); 469 | } catch (err) { 470 | console.error('使用sharp转换图片失败:', err); 471 | 472 | // 如果sharp处理失败,可能是不支持的格式,直接保存原始文件 473 | console.log('尝试直接保存原始图片文件...'); 474 | fs.writeFileSync(pngPath, imgBuffer); 475 | } 476 | 477 | console.log(`图片已保存到: ${pngPath}`); 478 | 479 | return pngPath; 480 | } catch (err) { 481 | console.error('图片下载或转换过程出错:', err); 482 | console.error('错误详情:', err.stack); 483 | throw err; 484 | } 485 | } 486 | 487 | app.listen(PORT, () => { 488 | console.log(`✨服务已启动,访问 http://localhost:${PORT}`); 489 | console.log('- 请确保 .env 文件包含所有必需参数'); 490 | console.log('- 在浏览器打开上面的地址,输入提示词生成图片'); 491 | }); --------------------------------------------------------------------------------