返回 oh-my-ppt
seedream.ts
根目录 / src / main / agent-runtime / provider / image / providers / seedream.ts
1 import log from 'electron-log/main.js'
2 import type {
3 ImageGenerationProviderAdapter,
4 ImageGenerationResult,
5 ResolvedImageModelConfig
6 } from '../types'
7 import { collectImageResults, joinUrl, readJsonResponse, readRecord, readString } from './utils'
8 import { resolveConfiguredDefaultImageSize } from './default-size'
9
10 const DEFAULT_BASE_URL = 'https://ark.cn-beijing.volces.com'
11 const DEFAULT_ENDPOINT_PATH = '/api/v3/images/generations'
12 const DEFAULT_MODEL = 'doubao-seedream-5-0-260128'
13 const LOG_TAG = 'seedream'
14 const LABEL = 'Seedream'
15 const DEFAULT_SIZE = '2K'
16
17 // M3b and the manual image panel use semantic aspect ratios. Seedream accepts a
18 // pixel dimension or a quality tier, so map the former to balanced defaults.
19 const DEFAULT_SIZE_BY_ASPECT: Record<string, string> = {
20 '1:1': '1024x1024',
21 '16:9': '1536x864',
22 '4:3': '1280x960',
23 '9:16': '864x1536',
24 '3:4': '960x1280'
25 }
26
27 const buildEndpoint = (config: ResolvedImageModelConfig): string => {
28 const endpoint = readString(config.modelConfig, 'endpoint')
29 if (endpoint) return endpoint
30 const baseUrl = readString(config.modelConfig, 'baseUrl')
31 if (baseUrl) {
32 try {
33 const parsed = new URL(baseUrl)
34 if (parsed.pathname && parsed.pathname !== '/') return baseUrl
35 } catch {
36 return baseUrl
37 }
38 }
39 return joinUrl(baseUrl || DEFAULT_BASE_URL, DEFAULT_ENDPOINT_PATH)
40 }
41
42 const resolveSize = (config: ResolvedImageModelConfig, inputSize: string): string => {
43 const configuredSize = readString(config.modelConfig, 'size') || readString(config.modelConfig, 'imageSize')
44 const size = configuredSize || inputSize
45 if (!size) throw new Error(`${LABEL} 需要 size,请在模型配置里填写 sizes 并选择一个值。`)
46 return DEFAULT_SIZE_BY_ASPECT[size.trim().toLowerCase()] || size
47 }
48
49 const readNumber = (config: ResolvedImageModelConfig, key: string): number | undefined => {
50 const value = Number(config.modelConfig[key])
51 return Number.isFinite(value) ? value : undefined
52 }
53
54 const readBoolean = (config: ResolvedImageModelConfig, key: string): boolean | undefined => {
55 const value = config.modelConfig[key]
56 if (typeof value === 'boolean') return value
57 if (typeof value === 'string') {
58 const normalized = value.trim().toLowerCase()
59 if (normalized === 'true') return true
60 if (normalized === 'false') return false
61 }
62 return undefined
63 }
64
65 const readBooleanWithDefault = (
66 config: ResolvedImageModelConfig,
67 key: string,
68 fallback: boolean
69 ): boolean => readBoolean(config, key) ?? fallback
70
71 const resolveResponseFormat = (config: ResolvedImageModelConfig): 'url' | 'b64_json' => {
72 const value =
73 readString(config.modelConfig, 'response_format') ||
74 readString(config.modelConfig, 'responseFormat')
75 return value === 'b64_json' ? 'b64_json' : 'url'
76 }
77
78 const buildOptionalParameters = (
79 config: ResolvedImageModelConfig,
80 input: Parameters<ImageGenerationProviderAdapter['generate']>[1]
81 ): Record<string, unknown> => {
82 const params: Record<string, unknown> = {
83 sequential_image_generation:
84 readString(config.modelConfig, 'sequential_image_generation') ||
85 readString(config.modelConfig, 'sequentialImageGeneration') ||
86 'disabled',
87 stream: readBooleanWithDefault(config, 'stream', false)
88 }
89 if (typeof input.seed === 'number') params.seed = input.seed
90 if (input.negativePrompt) params.negative_prompt = input.negativePrompt
91
92 const guidanceScale = readNumber(config, 'guidanceScale') ?? readNumber(config, 'guidance_scale')
93 if (guidanceScale !== undefined) params.guidance_scale = guidanceScale
94
95 const watermark = readBoolean(config, 'watermark')
96 if (watermark !== undefined) params.watermark = watermark
97
98 return params
99 }
100
101 const collectSeedreamImages = async (
102 payload: unknown,
103 signal?: AbortSignal
104 ): Promise<ImageGenerationResult[]> => collectImageResults(payload, signal)
105
106 const toErrorMessage = (error: unknown): string =>
107 error instanceof Error ? error.message : String(error)
108
109 export const seedreamAdapter: ImageGenerationProviderAdapter = {
110 getDefaultSize(config) {
111 return resolveConfiguredDefaultImageSize(config) || DEFAULT_SIZE
112 },
113
114 async generate(config, input) {
115 const startedAt = Date.now()
116 const endpoint = buildEndpoint(config)
117 const model = readString(config.modelConfig, 'model') || DEFAULT_MODEL
118 const apiKey = readString(config.modelConfig, 'apiKey')
119 if (!apiKey) throw new Error(`${LABEL} 需要 API Key。`)
120
121 const size = resolveSize(config, input.size)
122 const responseFormat = resolveResponseFormat(config)
123 const requestBody = readRecord(config.modelConfig.requestBody)
124 const headers = readRecord(config.modelConfig.headers) as Record<string, string>
125 const body = {
126 model,
127 prompt: input.prompt,
128 size,
129 n: input.count,
130 response_format: responseFormat,
131 ...buildOptionalParameters(config, input),
132 ...requestBody
133 }
134
135 log.info(`[images:${LOG_TAG}] generation start`, {
136 configId: config.id,
137 configName: config.name,
138 model,
139 endpoint,
140 size,
141 count: input.count,
142 responseFormat,
143 promptLength: input.prompt.length,
144 hasSeed: typeof input.seed === 'number',
145 requestBodyKeys: Object.keys(requestBody).sort()
146 })
147
148 try {
149 const response = await fetch(endpoint, {
150 method: 'POST',
151 signal: input.signal,
152 headers: {
153 authorization: `Bearer ${apiKey}`,
154 'content-type': 'application/json',
155 ...headers
156 },
157 body: JSON.stringify(body)
158 })
159 log.info(`[images:${LOG_TAG}] request end`, {
160 model,
161 status: response.status,
162 ok: response.ok,
163 elapsedMs: Date.now() - startedAt
164 })
165 const payload = await readJsonResponse(response)
166 const results = await collectSeedreamImages(payload, input.signal)
167 if (results.length === 0) throw new Error(`${LABEL} 未返回图片`)
168 log.info(`[images:${LOG_TAG}] generation completed`, {
169 model,
170 resultCount: results.length,
171 elapsedMs: Date.now() - startedAt
172 })
173 return results.slice(0, input.count)
174 } catch (error) {
175 log.error(`[images:${LOG_TAG}] generation failed`, {
176 model,
177 message: toErrorMessage(error),
178 elapsedMs: Date.now() - startedAt
179 })
180 throw error
181 }
182 }
183 }
184
184 lines TYPESCRIPT