486CHD commited on
Commit
935a74e
·
verified ·
1 Parent(s): 8194561

Upload 13 files

Browse files
Files changed (2) hide show
  1. index.html +63 -39
  2. 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, // 完整的 data:image/... 字符串
236
- thumb: item.thumb || null, // 缩略图(用于加速列表渲染)
237
- inputImages: item.inputImages || [],
238
- timestamp: Date.now()
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
- LazyImageLoader.observe(img, item.image);
 
1098
  img.alt = item.prompt || '创意作品';
1099
  card.appendChild(img);
1100
 
@@ -1173,15 +1176,29 @@
1173
  }
1174
 
1175
  try {
1176
- const res = await fetch('/api/public-gallery', {
1177
- method: 'POST',
1178
- headers: { 'Content-Type': 'application/json' },
1179
- body: JSON.stringify({
1180
- prompt: item.prompt,
1181
- image: item.image,
1182
- inputImages: item.inputImages || []
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
- if (!data.image || !data.image.startsWith('data:image')) {
1358
- throw new Error("\u8fd4\u56de\u7684\u56fe\u7247\u6570\u636e\u65e0\u6548");
1359
- }
1360
-
 
 
1361
  let thumb = null;
1362
  if (!Device.isMobile) {
1363
- thumb = await ImageHandler.createThumbnail(data.image, 768);
1364
  }
1365
 
1366
  const newItem = {
1367
  id: `temp-${Date.now()}`,
1368
  prompt: prompt,
1369
- image: data.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: data.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
- console.log(`[${new Date().toISOString()}] Image generated`);
573
-
574
- recordOnce(true);
575
- res.json({
576
- success: true,
577
- image: imageData,
578
- prompt: trimmedPrompt,
579
- inputImages: uploadedImages,
580
- timestamp: new Date().toISOString()
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
- if (!image || typeof image !== 'string' || !ImageParser.isValidBase64Image(image)) {
653
- return failShare(400, '\u8bf7\u63d0\u4f9b\u6709\u6548\u7684\u56fe\u7247\u6570\u636e');
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);