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