| 1 | import { ipcMain } from 'electron' |
| 2 | import fs from 'fs' |
| 3 | import path from 'path' |
| 4 | import type { |
| 5 | GeneratedImageAsset, |
| 6 | ImageGenerationHistoryRecord, |
| 7 | ImageModelProvider |
| 8 | } from '@shared/image-generation' |
| 9 | import type { ImageGenerationHistoryRow } from '../db/database' |
| 10 | import type { IpcContext } from '../ipc/context' |
| 11 | import { allowLocalAssetRoot } from '../io/local-asset-roots' |
| 12 | |
| 13 | const VALID_IMAGE_PROVIDERS = [ |
| 14 | 'jimeng', |
| 15 | 'jimeng4', |
| 16 | 'agnes', |
| 17 | 'siliconflow', |
| 18 | 'openaiCompatible', |
| 19 | 'gemini', |
| 20 | 'seedream' |
| 21 | ] as const |
| 22 | |
| 23 | const resolveProvider = (provider: unknown): ImageModelProvider => { |
| 24 | if (VALID_IMAGE_PROVIDERS.includes(provider as ImageModelProvider)) { |
| 25 | return provider as ImageModelProvider |
| 26 | } |
| 27 | throw new Error('Unsupported image provider') |
| 28 | } |
| 29 | |
| 30 | const mimeFromFileName = (fileName: string): string => { |
| 31 | const lower = fileName.toLowerCase() |
| 32 | if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg' |
| 33 | if (lower.endsWith('.webp')) return 'image/webp' |
| 34 | if (lower.endsWith('.gif')) return 'image/gif' |
| 35 | return 'image/png' |
| 36 | } |
| 37 | |
| 38 | const readImagePaths = (value: string): string[] => { |
| 39 | try { |
| 40 | const parsed = JSON.parse(value) |
| 41 | return Array.isArray(parsed) |
| 42 | ? parsed |
| 43 | .map((item) => String(item || '').trim()) |
| 44 | .filter((item) => item.startsWith('./images/')) |
| 45 | .slice(0, 12) |
| 46 | : [] |
| 47 | } catch { |
| 48 | return [] |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | const buildHistoryRecord = async ( |
| 53 | ctx: IpcContext, |
| 54 | row: ImageGenerationHistoryRow |
| 55 | ): Promise<ImageGenerationHistoryRecord> => { |
| 56 | const projectDir = await ctx.resolveSessionProjectDir(row.sessionId) |
| 57 | const imagesDir = path.join(projectDir, 'images') |
| 58 | allowLocalAssetRoot(imagesDir) |
| 59 | const imagePaths = readImagePaths(row.imagePaths) |
| 60 | const provider = resolveProvider(row.provider) |
| 61 | const assets: GeneratedImageAsset[] = [] |
| 62 | for (const relativePath of imagePaths) { |
| 63 | const fileName = path.basename(relativePath) |
| 64 | const absolutePath = path.join(projectDir, relativePath.replace(/^\.\//, '')) |
| 65 | let size = 0 |
| 66 | try { |
| 67 | size = (await fs.promises.stat(absolutePath)).size |
| 68 | } catch { |
| 69 | size = 0 |
| 70 | } |
| 71 | assets.push({ |
| 72 | id: `${row.id}-${assets.length}`, |
| 73 | fileName, |
| 74 | originalName: fileName, |
| 75 | relativePath, |
| 76 | absolutePath, |
| 77 | mimeType: mimeFromFileName(fileName), |
| 78 | size, |
| 79 | prompt: row.prompt, |
| 80 | modelConfigId: row.modelConfigId, |
| 81 | provider, |
| 82 | model: row.model, |
| 83 | pageId: row.pageId, |
| 84 | createdAt: row.createdAt |
| 85 | }) |
| 86 | } |
| 87 | return { |
| 88 | id: row.id, |
| 89 | sessionId: row.sessionId, |
| 90 | pageId: row.pageId, |
| 91 | prompt: row.prompt, |
| 92 | imagePaths, |
| 93 | assets, |
| 94 | modelConfigId: row.modelConfigId, |
| 95 | provider, |
| 96 | model: row.model, |
| 97 | createdAt: row.createdAt |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | export function registerImageGenerationHistoryHandlers(ctx: IpcContext): void { |
| 102 | ipcMain.handle('images:listHistory', async (_event, payload) => { |
| 103 | const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {} |
| 104 | const sessionId = typeof record.sessionId === 'string' ? record.sessionId.trim() : '' |
| 105 | const pageId = typeof record.pageId === 'string' ? record.pageId.trim() : '' |
| 106 | if (!sessionId || !pageId) return [] |
| 107 | const rows = await ctx.db.listImageGenerationHistories(sessionId, pageId) |
| 108 | return Promise.all(rows.map((row) => buildHistoryRecord(ctx, row))) |
| 109 | }) |
| 110 | } |
| 111 |