Spaces:
Running
Running
Upload 13 files
Browse files- index.html +63 -39
- server.js +126 -41
index.html
CHANGED
|
@@ -230,13 +230,15 @@
|
|
| 230 |
const store = tx.objectStore(STORE_NAME);
|
| 231 |
|
| 232 |
// 直接存储完整对象,不做任何转换
|
| 233 |
-
const record = {
|
| 234 |
-
prompt: item.prompt,
|
| 235 |
-
image: item.image, //
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
| 240 |
|
| 241 |
const request = store.add(record);
|
| 242 |
request.onsuccess = () => resolve(request.result);
|
|
@@ -689,8 +691,8 @@
|
|
| 689 |
img.loading = 'lazy';
|
| 690 |
img.decoding = 'async';
|
| 691 |
img.fetchPriority = 'low';
|
| 692 |
-
const displaySrc = item.thumb || item.image;
|
| 693 |
-
LazyImageLoader.observe(img, displaySrc);
|
| 694 |
img.alt = `作品 ${item.id}`;
|
| 695 |
img.onerror = () => {
|
| 696 |
console.error('图片加载失败, ID:', item.id);
|
|
@@ -777,14 +779,14 @@
|
|
| 777 |
}
|
| 778 |
},
|
| 779 |
|
| 780 |
-
downloadImage(item) {
|
| 781 |
-
const link = document.createElement('a');
|
| 782 |
-
link.href = item.image || item.thumb;
|
| 783 |
-
link.download = `banana-pro-${item.id}-${Date.now()}.png`;
|
| 784 |
-
document.body.appendChild(link);
|
| 785 |
-
link.click();
|
| 786 |
-
document.body.removeChild(link);
|
| 787 |
-
},
|
| 788 |
|
| 789 |
async deleteItem(id) {
|
| 790 |
if (!confirm('确定要删除这张图片吗?')) return;
|
|
@@ -837,7 +839,7 @@
|
|
| 837 |
AppState.currentModalItem = item;
|
| 838 |
|
| 839 |
// 设置主图 - 直接赋值
|
| 840 |
-
this.imgEl.src = item.image || item.thumb || '';
|
| 841 |
|
| 842 |
// 设置提示词
|
| 843 |
this.promptEl.textContent = item.prompt;
|
|
@@ -1094,7 +1096,8 @@
|
|
| 1094 |
img.loading = 'lazy';
|
| 1095 |
img.decoding = 'async';
|
| 1096 |
img.fetchPriority = 'low';
|
| 1097 |
-
|
|
|
|
| 1098 |
img.alt = item.prompt || '创意作品';
|
| 1099 |
card.appendChild(img);
|
| 1100 |
|
|
@@ -1173,15 +1176,29 @@
|
|
| 1173 |
}
|
| 1174 |
|
| 1175 |
try {
|
| 1176 |
-
const
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1185 |
|
| 1186 |
const data = await res.json();
|
| 1187 |
|
|
@@ -1340,10 +1357,11 @@
|
|
| 1340 |
const response = await fetch('/api/generate', {
|
| 1341 |
method: 'POST',
|
| 1342 |
headers: { 'Content-Type': 'application/json' },
|
| 1343 |
-
body: JSON.stringify({
|
| 1344 |
-
prompt: prompt,
|
| 1345 |
-
images: AppState.currentImages
|
| 1346 |
-
|
|
|
|
| 1347 |
signal: controller.signal
|
| 1348 |
});
|
| 1349 |
clearTimeout(timeoutId);
|
|
@@ -1354,19 +1372,23 @@
|
|
| 1354 |
throw new Error(data.message || "\u751f\u6210\u5931\u8d25");
|
| 1355 |
}
|
| 1356 |
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
|
|
|
|
|
|
| 1361 |
let thumb = null;
|
| 1362 |
if (!Device.isMobile) {
|
| 1363 |
-
thumb = await ImageHandler.createThumbnail(
|
| 1364 |
}
|
| 1365 |
|
| 1366 |
const newItem = {
|
| 1367 |
id: `temp-${Date.now()}`,
|
| 1368 |
prompt: prompt,
|
| 1369 |
-
image:
|
|
|
|
|
|
|
| 1370 |
thumb: thumb,
|
| 1371 |
inputImages: [...AppState.currentImages],
|
| 1372 |
timestamp: Date.now()
|
|
@@ -1377,7 +1399,9 @@
|
|
| 1377 |
|
| 1378 |
const savePayload = {
|
| 1379 |
prompt: prompt,
|
| 1380 |
-
image:
|
|
|
|
|
|
|
| 1381 |
thumb: thumb,
|
| 1382 |
inputImages: [...AppState.currentImages]
|
| 1383 |
};
|
|
|
|
| 230 |
const store = tx.objectStore(STORE_NAME);
|
| 231 |
|
| 232 |
// 直接存储完整对象,不做任何转换
|
| 233 |
+
const record = {
|
| 234 |
+
prompt: item.prompt,
|
| 235 |
+
image: item.image, // data:image/... 或 /generated/... URL
|
| 236 |
+
imageUrl: item.imageUrl || null,
|
| 237 |
+
imageId: item.imageId || null,
|
| 238 |
+
thumb: item.thumb || null, // 缩略图(用于加速列表渲染)
|
| 239 |
+
inputImages: item.inputImages || [],
|
| 240 |
+
timestamp: Date.now()
|
| 241 |
+
};
|
| 242 |
|
| 243 |
const request = store.add(record);
|
| 244 |
request.onsuccess = () => resolve(request.result);
|
|
|
|
| 691 |
img.loading = 'lazy';
|
| 692 |
img.decoding = 'async';
|
| 693 |
img.fetchPriority = 'low';
|
| 694 |
+
const displaySrc = item.thumb || item.image || item.imageUrl;
|
| 695 |
+
LazyImageLoader.observe(img, displaySrc);
|
| 696 |
img.alt = `作品 ${item.id}`;
|
| 697 |
img.onerror = () => {
|
| 698 |
console.error('图片加载失败, ID:', item.id);
|
|
|
|
| 779 |
}
|
| 780 |
},
|
| 781 |
|
| 782 |
+
downloadImage(item) {
|
| 783 |
+
const link = document.createElement('a');
|
| 784 |
+
link.href = item.image || item.imageUrl || item.thumb;
|
| 785 |
+
link.download = `banana-pro-${item.id}-${Date.now()}.png`;
|
| 786 |
+
document.body.appendChild(link);
|
| 787 |
+
link.click();
|
| 788 |
+
document.body.removeChild(link);
|
| 789 |
+
},
|
| 790 |
|
| 791 |
async deleteItem(id) {
|
| 792 |
if (!confirm('确定要删除这张图片吗?')) return;
|
|
|
|
| 839 |
AppState.currentModalItem = item;
|
| 840 |
|
| 841 |
// 设置主图 - 直接赋值
|
| 842 |
+
this.imgEl.src = item.image || item.imageUrl || item.thumb || '';
|
| 843 |
|
| 844 |
// 设置提示词
|
| 845 |
this.promptEl.textContent = item.prompt;
|
|
|
|
| 1096 |
img.loading = 'lazy';
|
| 1097 |
img.decoding = 'async';
|
| 1098 |
img.fetchPriority = 'low';
|
| 1099 |
+
const imageSrc = item.image || item.imageUrl;
|
| 1100 |
+
LazyImageLoader.observe(img, imageSrc);
|
| 1101 |
img.alt = item.prompt || '创意作品';
|
| 1102 |
card.appendChild(img);
|
| 1103 |
|
|
|
|
| 1176 |
}
|
| 1177 |
|
| 1178 |
try {
|
| 1179 |
+
const payload = {
|
| 1180 |
+
prompt: item.prompt,
|
| 1181 |
+
inputImages: item.inputImages || []
|
| 1182 |
+
};
|
| 1183 |
+
|
| 1184 |
+
const sourceImage = item.image || item.imageUrl;
|
| 1185 |
+
if (sourceImage && typeof sourceImage === 'string') {
|
| 1186 |
+
if (sourceImage.startsWith('data:image')) {
|
| 1187 |
+
payload.image = sourceImage;
|
| 1188 |
+
} else {
|
| 1189 |
+
payload.imageUrl = sourceImage;
|
| 1190 |
+
}
|
| 1191 |
+
}
|
| 1192 |
+
|
| 1193 |
+
if (item.imageId) {
|
| 1194 |
+
payload.imageId = item.imageId;
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
const res = await fetch('/api/public-gallery', {
|
| 1198 |
+
method: 'POST',
|
| 1199 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1200 |
+
body: JSON.stringify(payload)
|
| 1201 |
+
});
|
| 1202 |
|
| 1203 |
const data = await res.json();
|
| 1204 |
|
|
|
|
| 1357 |
const response = await fetch('/api/generate', {
|
| 1358 |
method: 'POST',
|
| 1359 |
headers: { 'Content-Type': 'application/json' },
|
| 1360 |
+
body: JSON.stringify({
|
| 1361 |
+
prompt: prompt,
|
| 1362 |
+
images: AppState.currentImages,
|
| 1363 |
+
preferUrl: Device.isMobile
|
| 1364 |
+
}),
|
| 1365 |
signal: controller.signal
|
| 1366 |
});
|
| 1367 |
clearTimeout(timeoutId);
|
|
|
|
| 1372 |
throw new Error(data.message || "\u751f\u6210\u5931\u8d25");
|
| 1373 |
}
|
| 1374 |
|
| 1375 |
+
const imageSrc = data.imageUrl || data.image;
|
| 1376 |
+
if (!imageSrc || typeof imageSrc !== 'string') {
|
| 1377 |
+
throw new Error("\u8fd4\u56de\u7684\u56fe\u7247\u6570\u636e\u65e0\u6548");
|
| 1378 |
+
}
|
| 1379 |
+
|
| 1380 |
+
const imageUrl = data.imageUrl || (imageSrc && !imageSrc.startsWith('data:image') ? imageSrc : null);
|
| 1381 |
let thumb = null;
|
| 1382 |
if (!Device.isMobile) {
|
| 1383 |
+
thumb = await ImageHandler.createThumbnail(imageSrc, 768);
|
| 1384 |
}
|
| 1385 |
|
| 1386 |
const newItem = {
|
| 1387 |
id: `temp-${Date.now()}`,
|
| 1388 |
prompt: prompt,
|
| 1389 |
+
image: imageSrc,
|
| 1390 |
+
imageUrl: imageUrl,
|
| 1391 |
+
imageId: data.imageId || null,
|
| 1392 |
thumb: thumb,
|
| 1393 |
inputImages: [...AppState.currentImages],
|
| 1394 |
timestamp: Date.now()
|
|
|
|
| 1399 |
|
| 1400 |
const savePayload = {
|
| 1401 |
prompt: prompt,
|
| 1402 |
+
image: imageSrc,
|
| 1403 |
+
imageUrl: imageUrl,
|
| 1404 |
+
imageId: data.imageId || null,
|
| 1405 |
thumb: thumb,
|
| 1406 |
inputImages: [...AppState.currentImages]
|
| 1407 |
};
|
server.js
CHANGED
|
@@ -52,19 +52,21 @@ console.log('[DEBUG] GEMINI_API_URL:', process.env.GEMINI_API_URL || process.env
|
|
| 52 |
console.log(`[DEBUG] 最终 CONFIG.apiUrl:`, CONFIG.apiUrl);
|
| 53 |
console.log('[DEBUG] Model:', CONFIG.modelName);
|
| 54 |
|
| 55 |
-
const DATA_DIR = path.join(__dirname, 'data');
|
| 56 |
-
const PUBLIC_GALLERY_FILE = path.join(DATA_DIR, 'public-gallery.json');
|
| 57 |
-
const STATS_FILE = path.join(DATA_DIR, 'usage-stats.json');
|
|
|
|
| 58 |
|
| 59 |
// ============================================
|
| 60 |
// Express 应用初始化
|
| 61 |
// ============================================
|
| 62 |
const app = express();
|
| 63 |
|
| 64 |
-
app.use(express.json({ limit: '200mb' }));
|
| 65 |
-
app.use(express.urlencoded({ extended: true, limit: '200mb' }));
|
| 66 |
-
app.use(cookieParser());
|
| 67 |
-
app.use(express.static(__dirname));
|
|
|
|
| 68 |
|
| 69 |
// ============================================
|
| 70 |
// 认证中间件
|
|
@@ -256,8 +258,56 @@ const generateId = () => {
|
|
| 256 |
return crypto.randomBytes(16).toString('hex');
|
| 257 |
};
|
| 258 |
|
| 259 |
-
const generateDeleteToken = () => crypto.randomBytes(24).toString('hex');
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
const PublicGalleryStore = {
|
| 262 |
async ensureFile() {
|
| 263 |
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
@@ -514,11 +564,11 @@ app.get('/api/check-auth', (req, res) => {
|
|
| 514 |
});
|
| 515 |
|
| 516 |
// Generate image (multi-image)
|
| 517 |
-
app.post('/api/generate', authMiddleware, async (req, res) => {
|
| 518 |
-
const { prompt, images } = req.body;
|
| 519 |
-
const requestStart = Date.now();
|
| 520 |
-
StatsStore.activeRequests += 1;
|
| 521 |
-
let recorded = false;
|
| 522 |
|
| 523 |
const recordOnce = (ok, errorMessage) => {
|
| 524 |
if (recorded) return;
|
|
@@ -566,19 +616,45 @@ app.post('/api/generate', authMiddleware, async (req, res) => {
|
|
| 566 |
console.log('[DEBUG] Model:', CONFIG.modelName);
|
| 567 |
console.log(`[${new Date().toISOString()}] API key: ${CONFIG.apiKey.substring(0, 10)}...`);
|
| 568 |
|
| 569 |
-
const apiResponse = await APIService.generateImage(trimmedPrompt, uploadedImages);
|
| 570 |
-
const imageData = APIService.extractImageFromResponse(apiResponse);
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
|
| 583 |
} catch (error) {
|
| 584 |
recordOnce(false, error.message);
|
|
@@ -627,9 +703,9 @@ app.get('/api/public-gallery', async (req, res) => {
|
|
| 627 |
});
|
| 628 |
|
| 629 |
// Public gallery - publish
|
| 630 |
-
app.post('/api/public-gallery', authMiddleware, async (req, res) => {
|
| 631 |
-
const { prompt, image, inputImages } = req.body;
|
| 632 |
-
let shareRecorded = false;
|
| 633 |
|
| 634 |
const recordShareOnce = (ok, errorMessage) => {
|
| 635 |
if (shareRecorded) return;
|
|
@@ -649,9 +725,18 @@ app.post('/api/public-gallery', authMiddleware, async (req, res) => {
|
|
| 649 |
return failShare(400, '\u8bf7\u8f93\u5165\u6709\u6548\u7684\u63d0\u793a\u8bcd');
|
| 650 |
}
|
| 651 |
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
const sanitizedRefs = Array.isArray(inputImages)
|
| 657 |
? inputImages
|
|
@@ -659,14 +744,14 @@ app.post('/api/public-gallery', authMiddleware, async (req, res) => {
|
|
| 659 |
.slice(0, CONFIG.maxImages)
|
| 660 |
: [];
|
| 661 |
|
| 662 |
-
const entry = {
|
| 663 |
-
id: generateId(),
|
| 664 |
-
prompt: prompt.trim(),
|
| 665 |
-
image,
|
| 666 |
-
inputImages: sanitizedRefs,
|
| 667 |
-
timestamp: new Date().toISOString(),
|
| 668 |
-
deleteToken: generateDeleteToken()
|
| 669 |
-
};
|
| 670 |
|
| 671 |
try {
|
| 672 |
await PublicGalleryStore.add(entry);
|
|
|
|
| 52 |
console.log(`[DEBUG] 最终 CONFIG.apiUrl:`, CONFIG.apiUrl);
|
| 53 |
console.log('[DEBUG] Model:', CONFIG.modelName);
|
| 54 |
|
| 55 |
+
const DATA_DIR = path.join(__dirname, 'data');
|
| 56 |
+
const PUBLIC_GALLERY_FILE = path.join(DATA_DIR, 'public-gallery.json');
|
| 57 |
+
const STATS_FILE = path.join(DATA_DIR, 'usage-stats.json');
|
| 58 |
+
const GENERATED_DIR = path.join(DATA_DIR, 'generated');
|
| 59 |
|
| 60 |
// ============================================
|
| 61 |
// Express 应用初始化
|
| 62 |
// ============================================
|
| 63 |
const app = express();
|
| 64 |
|
| 65 |
+
app.use(express.json({ limit: '200mb' }));
|
| 66 |
+
app.use(express.urlencoded({ extended: true, limit: '200mb' }));
|
| 67 |
+
app.use(cookieParser());
|
| 68 |
+
app.use(express.static(__dirname));
|
| 69 |
+
app.use('/generated', express.static(GENERATED_DIR));
|
| 70 |
|
| 71 |
// ============================================
|
| 72 |
// 认证中间件
|
|
|
|
| 258 |
return crypto.randomBytes(16).toString('hex');
|
| 259 |
};
|
| 260 |
|
| 261 |
+
const generateDeleteToken = () => crypto.randomBytes(24).toString('hex');
|
| 262 |
+
|
| 263 |
+
const mimeToExt = (mimeType) => {
|
| 264 |
+
const normalized = String(mimeType || '').toLowerCase();
|
| 265 |
+
if (normalized === 'image/jpeg' || normalized === 'image/jpg') return 'jpg';
|
| 266 |
+
if (normalized === 'image/webp') return 'webp';
|
| 267 |
+
if (normalized === 'image/gif') return 'gif';
|
| 268 |
+
return 'png';
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
const GeneratedImageStore = {
|
| 272 |
+
async ensureDir() {
|
| 273 |
+
await fs.mkdir(GENERATED_DIR, { recursive: true });
|
| 274 |
+
},
|
| 275 |
+
|
| 276 |
+
async saveDataUrl(dataUrl) {
|
| 277 |
+
if (!dataUrl || typeof dataUrl !== 'string') return null;
|
| 278 |
+
const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
|
| 279 |
+
if (!match) return null;
|
| 280 |
+
|
| 281 |
+
const mimeType = match[1];
|
| 282 |
+
const base64Data = match[2];
|
| 283 |
+
const ext = mimeToExt(mimeType);
|
| 284 |
+
const id = generateId();
|
| 285 |
+
const filename = `${id}.${ext}`;
|
| 286 |
+
const filePath = path.join(GENERATED_DIR, filename);
|
| 287 |
+
|
| 288 |
+
await this.ensureDir();
|
| 289 |
+
await fs.writeFile(filePath, Buffer.from(base64Data, 'base64'));
|
| 290 |
+
|
| 291 |
+
return {
|
| 292 |
+
id: filename,
|
| 293 |
+
url: `/generated/${filename}`,
|
| 294 |
+
mimeType
|
| 295 |
+
};
|
| 296 |
+
},
|
| 297 |
+
|
| 298 |
+
async resolveUrl(id) {
|
| 299 |
+
if (!id || typeof id !== 'string') return null;
|
| 300 |
+
const safeName = path.basename(id);
|
| 301 |
+
const filePath = path.join(GENERATED_DIR, safeName);
|
| 302 |
+
try {
|
| 303 |
+
await fs.access(filePath);
|
| 304 |
+
return `/generated/${safeName}`;
|
| 305 |
+
} catch {
|
| 306 |
+
return null;
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
};
|
| 310 |
+
|
| 311 |
const PublicGalleryStore = {
|
| 312 |
async ensureFile() {
|
| 313 |
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
|
|
| 564 |
});
|
| 565 |
|
| 566 |
// Generate image (multi-image)
|
| 567 |
+
app.post('/api/generate', authMiddleware, async (req, res) => {
|
| 568 |
+
const { prompt, images, preferUrl } = req.body;
|
| 569 |
+
const requestStart = Date.now();
|
| 570 |
+
StatsStore.activeRequests += 1;
|
| 571 |
+
let recorded = false;
|
| 572 |
|
| 573 |
const recordOnce = (ok, errorMessage) => {
|
| 574 |
if (recorded) return;
|
|
|
|
| 616 |
console.log('[DEBUG] Model:', CONFIG.modelName);
|
| 617 |
console.log(`[${new Date().toISOString()}] API key: ${CONFIG.apiKey.substring(0, 10)}...`);
|
| 618 |
|
| 619 |
+
const apiResponse = await APIService.generateImage(trimmedPrompt, uploadedImages);
|
| 620 |
+
const imageData = APIService.extractImageFromResponse(apiResponse);
|
| 621 |
+
const wantsUrl = Boolean(preferUrl);
|
| 622 |
+
let imageUrl = null;
|
| 623 |
+
let imageId = null;
|
| 624 |
+
|
| 625 |
+
if (typeof imageData === 'string' && imageData.startsWith('data:image/')) {
|
| 626 |
+
if (wantsUrl) {
|
| 627 |
+
try {
|
| 628 |
+
const saved = await GeneratedImageStore.saveDataUrl(imageData);
|
| 629 |
+
if (saved) {
|
| 630 |
+
imageUrl = saved.url;
|
| 631 |
+
imageId = saved.id;
|
| 632 |
+
}
|
| 633 |
+
} catch (saveError) {
|
| 634 |
+
console.error(`[${new Date().toISOString()}] Save generated image failed:`, saveError);
|
| 635 |
+
}
|
| 636 |
+
}
|
| 637 |
+
} else if (typeof imageData === 'string') {
|
| 638 |
+
imageUrl = imageData;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
console.log(`[${new Date().toISOString()}] Image generated`);
|
| 642 |
+
|
| 643 |
+
recordOnce(true);
|
| 644 |
+
const responsePayload = {
|
| 645 |
+
success: true,
|
| 646 |
+
imageUrl,
|
| 647 |
+
imageId,
|
| 648 |
+
prompt: trimmedPrompt,
|
| 649 |
+
inputImages: uploadedImages,
|
| 650 |
+
timestamp: new Date().toISOString()
|
| 651 |
+
};
|
| 652 |
+
|
| 653 |
+
if (!wantsUrl || !imageUrl) {
|
| 654 |
+
responsePayload.image = imageData;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
res.json(responsePayload);
|
| 658 |
|
| 659 |
} catch (error) {
|
| 660 |
recordOnce(false, error.message);
|
|
|
|
| 703 |
});
|
| 704 |
|
| 705 |
// Public gallery - publish
|
| 706 |
+
app.post('/api/public-gallery', authMiddleware, async (req, res) => {
|
| 707 |
+
const { prompt, image, imageUrl, imageId, inputImages } = req.body;
|
| 708 |
+
let shareRecorded = false;
|
| 709 |
|
| 710 |
const recordShareOnce = (ok, errorMessage) => {
|
| 711 |
if (shareRecorded) return;
|
|
|
|
| 725 |
return failShare(400, '\u8bf7\u8f93\u5165\u6709\u6548\u7684\u63d0\u793a\u8bcd');
|
| 726 |
}
|
| 727 |
|
| 728 |
+
let finalImage = null;
|
| 729 |
+
if (image && typeof image === 'string' && ImageParser.isValidBase64Image(image)) {
|
| 730 |
+
finalImage = image;
|
| 731 |
+
} else if (imageUrl && typeof imageUrl === 'string') {
|
| 732 |
+
finalImage = imageUrl.trim();
|
| 733 |
+
} else if (imageId && typeof imageId === 'string') {
|
| 734 |
+
finalImage = await GeneratedImageStore.resolveUrl(imageId);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
if (!finalImage) {
|
| 738 |
+
return failShare(400, '\u8bf7\u63d0\u4f9b\u6709\u6548\u7684\u56fe\u7247\u6570\u636e');
|
| 739 |
+
}
|
| 740 |
|
| 741 |
const sanitizedRefs = Array.isArray(inputImages)
|
| 742 |
? inputImages
|
|
|
|
| 744 |
.slice(0, CONFIG.maxImages)
|
| 745 |
: [];
|
| 746 |
|
| 747 |
+
const entry = {
|
| 748 |
+
id: generateId(),
|
| 749 |
+
prompt: prompt.trim(),
|
| 750 |
+
image: finalImage,
|
| 751 |
+
inputImages: sanitizedRefs,
|
| 752 |
+
timestamp: new Date().toISOString(),
|
| 753 |
+
deleteToken: generateDeleteToken()
|
| 754 |
+
};
|
| 755 |
|
| 756 |
try {
|
| 757 |
await PublicGalleryStore.add(entry);
|