返回 oh-my-ppt
image-model-handlers.ts
根目录 / src / main / config / image-model-handlers.ts
1 import { ipcMain } from 'electron'
2 import log from 'electron-log/main.js'
3 import type { ImageModelProvider, ImageModelVerificationResult } from '@shared/image-generation'
4 import {
5 resolveImageGenerationProvider,
6 type ResolvedImageModelConfig
7 } from '../agent-runtime/provider/image'
8 import type { IpcContext } from '../ipc/context'
9 import { readAppLocale, uiText } from './locale-utils'
10
11 const IMAGE_MODEL_VERIFY_PROMPT = '生成一只卡通猫'
12 const IMAGE_MODEL_VERIFY_TIMEOUT_MS = 120_000
13
14 const VALID_IMAGE_PROVIDERS = [
15 'jimeng',
16 'jimeng4',
17 'agnes',
18 'siliconflow',
19 'openaiCompatible',
20 'gemini',
21 'seedream'
22 ] as const
23
24 const resolveProvider = (provider: unknown): ImageModelProvider => {
25 if (VALID_IMAGE_PROVIDERS.includes(provider as ImageModelProvider)) {
26 return provider as ImageModelProvider
27 }
28 throw new Error('Unsupported image provider')
29 }
30
31 const normalizeModelConfig = (value: unknown): string => {
32 const text = typeof value === 'string' ? value.trim() : ''
33 if (!text) return '{}'
34 try {
35 const parsed = JSON.parse(text)
36 if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return '{}'
37 return text
38 } catch {
39 return '{}'
40 }
41 }
42
43 const toPreviewDataUrl = (bytes: Buffer, mimeType: string): string => {
44 const safeMimeType = /^image\/[a-z0-9.+-]+$/i.test(mimeType) ? mimeType : 'image/png'
45 return `data:${safeMimeType};base64,${bytes.toString('base64')}`
46 }
47
48 const createVerificationConfig = (
49 provider: ImageModelProvider,
50 modelConfig: string
51 ): ResolvedImageModelConfig => ({
52 id: 'image-model-verification',
53 name: 'Image model verification',
54 provider,
55 active: false,
56 modelConfig: JSON.parse(modelConfig) as Record<string, unknown>
57 })
58
59 export function registerImageModelHandlers(ctx: IpcContext): void {
60 const { db, encryptApiKey, decryptApiKey } = ctx
61
62 ipcMain.handle('imageModels:list', async () => {
63 return (await db.listImageModelConfigs()).map((config) => ({
64 id: config.id,
65 name: config.name,
66 provider: resolveProvider(config.provider),
67 active: config.active === 1,
68 modelConfig: decryptApiKey(config.modelConfig || '{}'),
69 createdAt: config.createdAt,
70 updatedAt: config.updatedAt
71 }))
72 })
73
74 ipcMain.handle('imageModels:upsert', async (_event, payload) => {
75 const locale = await readAppLocale(ctx)
76 const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
77 const name = typeof record.name === 'string' ? record.name.trim() : ''
78 const provider = resolveProvider(record.provider)
79 const modelConfig = normalizeModelConfig(record.modelConfig)
80 const id =
81 typeof record.id === 'string' && record.id.trim().length > 0 ? record.id.trim() : undefined
82 if (!name) throw new Error(uiText(locale, '请填写生图模型名称。', 'Enter image model name.'))
83 if (modelConfig === '{}') {
84 throw new Error(uiText(locale, '请填写生图模型配置。', 'Enter image model config.'))
85 }
86 const savedId = await db.upsertImageModelConfig({
87 id,
88 name,
89 provider,
90 modelConfig: encryptApiKey(modelConfig),
91 active: record.active === true,
92 })
93 return { success: true, id: savedId }
94 })
95
96 ipcMain.handle('imageModels:setActive', async (_event, id) => {
97 const locale = await readAppLocale(ctx)
98 if (typeof id !== 'string' || id.trim().length === 0) {
99 throw new Error(uiText(locale, '生图模型配置 ID 不能为空。', 'Image model config ID is required.'))
100 }
101 try {
102 await db.setActiveImageModelConfig(id.trim())
103 } catch (error) {
104 if (error instanceof Error && error.message === 'Image model config does not exist') {
105 throw new Error(uiText(locale, '生图模型配置不存在。', 'Image model config does not exist.'))
106 }
107 throw error
108 }
109 return { success: true }
110 })
111
112 ipcMain.handle('imageModels:delete', async (_event, id) => {
113 const locale = await readAppLocale(ctx)
114 if (typeof id !== 'string' || id.trim().length === 0) {
115 throw new Error(uiText(locale, '生图模型配置 ID 不能为空。', 'Image model config ID is required.'))
116 }
117 try {
118 await db.deleteImageModelConfig(id.trim())
119 } catch (error) {
120 if (error instanceof Error && error.message === 'Image model config does not exist') {
121 throw new Error(uiText(locale, '生图模型配置不存在。', 'Image model config does not exist.'))
122 }
123 throw error
124 }
125 return { success: true }
126 })
127
128 ipcMain.handle('imageModels:verify', async (_event, payload) => {
129 const locale = await readAppLocale(ctx)
130 const record = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
131 const provider = resolveProvider(record.provider)
132 const modelConfig = normalizeModelConfig(record.modelConfig)
133 log.info('[imageModels:verify] received', { provider, hasConfig: modelConfig !== '{}' })
134 if (modelConfig === '{}') {
135 return {
136 valid: false,
137 message: uiText(locale, '请先填写生图模型配置。', 'Enter image model config first.')
138 } satisfies ImageModelVerificationResult
139 }
140
141 try {
142 const verificationConfig = createVerificationConfig(provider, modelConfig)
143 const adapter = resolveImageGenerationProvider(provider)
144 const size = adapter.getDefaultSize(verificationConfig)
145 const [image] = await adapter.generate(verificationConfig, {
146 prompt: IMAGE_MODEL_VERIFY_PROMPT,
147 count: 1,
148 size,
149 signal: AbortSignal.timeout(IMAGE_MODEL_VERIFY_TIMEOUT_MS)
150 })
151 if (!image || image.bytes.length === 0) {
152 return {
153 valid: false,
154 message: uiText(
155 locale,
156 '生图接口未返回可预览的图片。',
157 'The image endpoint returned no previewable image.'
158 )
159 } satisfies ImageModelVerificationResult
160 }
161 return {
162 valid: true,
163 message: uiText(locale, '已成功生成测试图片。', 'A test image was generated successfully.'),
164 previewDataUrl: toPreviewDataUrl(image.bytes, image.mimeType)
165 } satisfies ImageModelVerificationResult
166 } catch (error) {
167 const message = error instanceof Error && error.message ? error.message : String(error)
168 log.error('[imageModels:verify] failed', { provider, message })
169 return {
170 valid: false,
171 message
172 } satisfies ImageModelVerificationResult
173 }
174 })
175 }
176
176 lines TYPESCRIPT