├── {
├── .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 | });
--------------------------------------------------------------------------------