diff --git a/docs/image-mode.md b/docs/image-mode.md index b366831c..2d30df23 100644 --- a/docs/image-mode.md +++ b/docs/image-mode.md @@ -18,6 +18,10 @@ - provider: `seedream` - defaultModel: `doubao-seedream-4-0-250828` - apiKey: 读取 `VITE_SEEDREAM_API_KEY` | `VITE_ARK_API_KEY`(或 `process.env.ARK_API_KEY`) +- Seedream 5.0 Lite(image-seedream-50-lite) + - provider: `seedream` + - defaultModel: `doubao-seedream-5-0-260128` + - apiKey: 复用 `VITE_SEEDREAM_API_KEY` | `VITE_ARK_API_KEY`(或 `process.env.ARK_API_KEY`) > 提示:配置好以上环境变量后,内置图像模型将自动注入并按需启用。 diff --git a/packages/core/src/services/image-model/defaults.ts b/packages/core/src/services/image-model/defaults.ts index 07902ae6..4229cca0 100644 --- a/packages/core/src/services/image-model/defaults.ts +++ b/packages/core/src/services/image-model/defaults.ts @@ -17,21 +17,29 @@ const IMAGE_PROVIDER_ENV_KEYS = { cloudflare: ['VITE_CF_API_TOKEN'] } as const -/** - * 配置 ID 映射(保持现有 ID 不变以兼容用户数据) - * name 将从 provider.name 获取,无需硬编码 - */ -const IMAGE_CONFIG_IDS: Record = { - openrouter: 'image-openrouter-nanobanana', - gemini: 'image-gemini-nanobanana', - openai: 'image-openai-gpt', - siliconflow: 'image-siliconflow-kolors', - seedream: 'image-seedream', - dashscope: 'image-dashscope', - modelscope: 'image-modelscope', - cloudflare: 'image-cloudflare-flux-klein' +type BuiltinImageConfigSpec = { + providerId: keyof typeof IMAGE_PROVIDER_ENV_KEYS + configId: string + modelId?: string + displayName?: string } +/** + * 内置图像配置定义。 + * 允许一个 provider 生成多个内置配置,例如 Seedream 4.0 与 5.0 lite。 + */ +const IMAGE_BUILTIN_CONFIGS: readonly BuiltinImageConfigSpec[] = [ + { providerId: 'openrouter', configId: 'image-openrouter-nanobanana' }, + { providerId: 'gemini', configId: 'image-gemini-nanobanana' }, + { providerId: 'openai', configId: 'image-openai-gpt' }, + { providerId: 'siliconflow', configId: 'image-siliconflow-kolors' }, + { providerId: 'seedream', configId: 'image-seedream', modelId: 'doubao-seedream-4-0-250828' }, + { providerId: 'seedream', configId: 'image-seedream-50-lite', modelId: 'doubao-seedream-5-0-260128', displayName: 'Doubao Seedream 5.0 Lite' }, + { providerId: 'dashscope', configId: 'image-dashscope' }, + { providerId: 'modelscope', configId: 'image-modelscope' }, + { providerId: 'cloudflare', configId: 'image-cloudflare-flux-klein' } +] as const + /** * 特殊 baseURL 环境变量(仅需要覆盖的 Provider) */ @@ -78,14 +86,15 @@ export function getDefaultImageModels(registry?: IImageAdapterRegistry): Record< const result: Record = {} // 批量生成配置(与文本模型风格一致) - for (const [providerId, envKeys] of Object.entries(IMAGE_PROVIDER_ENV_KEYS)) { - const configId = IMAGE_CONFIG_IDS[providerId] - if (!configId) continue - + for (const builtinConfig of IMAGE_BUILTIN_CONFIGS) { + const providerId = builtinConfig.providerId + const envKeys = IMAGE_PROVIDER_ENV_KEYS[providerId] const adapter = adapterRegistry.getAdapter(providerId) const provider = adapter.getProvider() const models = adapterRegistry.getStaticModels(providerId) - const defaultModel = models[0] || adapter.buildDefaultModel(providerId) + const defaultModel = models.find(model => model.id === builtinConfig.modelId) + || models[0] + || adapter.buildDefaultModel(builtinConfig.modelId || providerId) // 获取 API Key(支持备选环境变量) const apiKey = getFirstEnvValue(envKeys) @@ -117,9 +126,9 @@ export function getDefaultImageModels(registry?: IImageAdapterRegistry): Record< return typeof value === 'string' ? value.trim().length > 0 : !!value }) - result[configId] = { - id: configId, - name: provider.name, // 从 provider 获取名称,不再硬编码 + result[builtinConfig.configId] = { + id: builtinConfig.configId, + name: builtinConfig.displayName || provider.name, providerId, modelId: defaultModel.id, enabled, @@ -139,7 +148,7 @@ export function getDefaultImageModels(registry?: IImageAdapterRegistry): Record< * 用于判断某个配置是否为内置模型(而非用户自定义) */ export function getBuiltinImageConfigIds(): string[] { - return Object.values(IMAGE_CONFIG_IDS) + return IMAGE_BUILTIN_CONFIGS.map(config => config.configId) } // 直接导出所有图像模型配置(保持向后兼容,与文本模型风格一致) diff --git a/packages/core/src/services/image/adapters/seedream.ts b/packages/core/src/services/image/adapters/seedream.ts index fd6ac6e7..f8133d76 100644 --- a/packages/core/src/services/image/adapters/seedream.ts +++ b/packages/core/src/services/image/adapters/seedream.ts @@ -5,10 +5,170 @@ import type { ImageModel, ImageRequest, ImageResult, - ImageModelConfig + ImageModelConfig, + ImageParameterDefinition } from '../types' import { IMAGE_ERROR_CODES } from '../../../constants/error-codes' +const SEEDREAM_40_MODEL_ID = 'doubao-seedream-4-0-250828' +const SEEDREAM_45_MODEL_ID = 'doubao-seedream-4-5-251128' +const SEEDREAM_50_LITE_MODEL_ID = 'doubao-seedream-5-0-260128' + +const SEEDREAM_40_SIZE_VALUES = ['1K', '2K', '4K', '1024x1024', '512x512', '768x768', '1024x768', '768x1024'] as const +const SEEDREAM_45_SIZE_VALUES = ['2K', '4K', '2048x2048'] as const +const SEEDREAM_50_LITE_SIZE_VALUES = ['2K', '4K', '2048x2048'] as const + +type SeedreamModelSpec = { + id: string + name: string + description: string + capabilities: ImageModel['capabilities'] + parameterDefinitions: readonly ImageParameterDefinition[] + defaultParameterValues: Record +} + +const SEEDREAM_MODEL_SPECS: readonly SeedreamModelSpec[] = [ + { + id: SEEDREAM_40_MODEL_ID, + name: 'Doubao Seedream 4.0', + description: '火山方舟 Doubao Seedream 4.0 高质量图像生成模型', + capabilities: { + text2image: true, + image2image: true, + multiImage: true + }, + parameterDefinitions: [ + { + name: 'size', + labelKey: 'params.size.label', + descriptionKey: 'params.size.description', + type: 'string', + defaultValue: '2K', + allowedValues: [...SEEDREAM_40_SIZE_VALUES] + }, + { + name: 'sequential_image_generation', + labelKey: 'params.sequentialGeneration.label', + descriptionKey: 'params.sequentialGeneration.description', + type: 'string', + defaultValue: 'disabled', + allowedValues: ['disabled'] + }, + { + name: 'response_format', + labelKey: 'params.responseFormat.label', + descriptionKey: 'params.responseFormat.description', + type: 'string', + defaultValue: 'b64_json', + allowedValues: ['b64_json', 'url'] + }, + { + name: 'watermark', + labelKey: 'params.watermark.label', + descriptionKey: 'params.watermark.description', + type: 'boolean', + defaultValue: false + } + ], + defaultParameterValues: { + size: '2K', + sequential_image_generation: 'disabled', + response_format: 'b64_json', + watermark: false + } + }, + { + id: SEEDREAM_45_MODEL_ID, + name: 'Doubao Seedream 4.5', + description: '火山方舟 Doubao Seedream 4.5 高质量图像生成模型', + capabilities: { + text2image: true, + image2image: true, + multiImage: true + }, + parameterDefinitions: [ + { + name: 'size', + labelKey: 'params.size.label', + descriptionKey: 'params.size.description', + type: 'string', + defaultValue: '2048x2048', + allowedValues: [...SEEDREAM_45_SIZE_VALUES] + }, + { + name: 'sequential_image_generation', + labelKey: 'params.sequentialGeneration.label', + descriptionKey: 'params.sequentialGeneration.description', + type: 'string', + defaultValue: 'disabled', + allowedValues: ['disabled'] + }, + { + name: 'response_format', + labelKey: 'params.responseFormat.label', + descriptionKey: 'params.responseFormat.description', + type: 'string', + defaultValue: 'b64_json', + allowedValues: ['b64_json', 'url'] + }, + { + name: 'watermark', + labelKey: 'params.watermark.label', + descriptionKey: 'params.watermark.description', + type: 'boolean', + defaultValue: false + } + ], + defaultParameterValues: { + size: '2048x2048', + sequential_image_generation: 'disabled', + response_format: 'b64_json', + watermark: false + } + }, + { + id: SEEDREAM_50_LITE_MODEL_ID, + name: 'Doubao Seedream 5.0 Lite', + description: '火山方舟 Doubao Seedream 5.0 lite 高质量图像生成模型', + capabilities: { + text2image: true, + image2image: true, + multiImage: false + }, + parameterDefinitions: [ + { + name: 'size', + labelKey: 'params.size.label', + descriptionKey: 'params.size.description', + type: 'string', + defaultValue: '2048x2048', + allowedValues: [...SEEDREAM_50_LITE_SIZE_VALUES] + }, + { + name: 'output_format', + labelKey: 'params.outputFormat.label', + descriptionKey: 'params.outputFormat.description', + type: 'string', + defaultValue: 'png', + allowedValues: ['png', 'jpeg', 'webp'] + }, + { + name: 'tools', + labelKey: 'params.tools.label', + descriptionKey: 'params.tools.description', + type: 'string', + defaultValue: [], + tags: ['string-array'] + } + ], + defaultParameterValues: { + size: '2048x2048', + output_format: 'png', + tools: [] + } + } +] as const + export class SeedreamImageAdapter extends AbstractImageProviderAdapter { protected normalizeBaseUrl(base: string): string { const trimmed = base.replace(/\/$/, '') @@ -36,106 +196,23 @@ export class SeedreamImageAdapter extends AbstractImageProviderAdapter { } getModels(): ImageModel[] { - // 返回静态的模型列表(只保留4.0版本) - return [ - { - id: 'doubao-seedream-4-0-250828', - name: 'Doubao Seedream 4.0', - description: '火山方舟 Doubao Seedream 4.0 高质量图像生成模型', - providerId: 'seedream', - capabilities: { - text2image: true, - image2image: true, - multiImage: true - }, - parameterDefinitions: [ - { - name: 'size', - labelKey: 'params.size.label', - descriptionKey: 'params.size.description', - type: 'string', - defaultValue: '2K', - allowedValues: ['1K', '2K', '4K', '1024x1024', '512x512', '768x768', '1024x768', '768x1024'] - }, - { - name: 'sequential_image_generation', - labelKey: 'params.sequentialGeneration.label', - descriptionKey: 'params.sequentialGeneration.description', - type: 'string', - defaultValue: 'disabled', - allowedValues: ['disabled'] - }, - { - name: 'response_format', - labelKey: 'params.responseFormat.label', - descriptionKey: 'params.responseFormat.description', - type: 'string', - defaultValue: 'b64_json', - allowedValues: ['b64_json', 'url'] - }, - { - name: 'watermark', - labelKey: 'params.watermark.label', - descriptionKey: 'params.watermark.description', - type: 'boolean', - defaultValue: false - } - ], - defaultParameterValues: { - size: '2K', - sequential_image_generation: 'disabled', - response_format: 'b64_json', - watermark: false - } - } - ] + return SEEDREAM_MODEL_SPECS.map((spec) => ({ + id: spec.id, + name: spec.name, + description: spec.description, + providerId: 'seedream', + capabilities: { ...spec.capabilities }, + parameterDefinitions: this.getParameterDefinitions(spec.id), + defaultParameterValues: this.getDefaultParameterValues(spec.id) + })) } - protected getParameterDefinitions(_modelId: string): readonly any[] { - // 所有模型使用统一的参数定义(只保留4.0版本) - return [ - { - name: 'size', - labelKey: 'params.size.label', - descriptionKey: 'params.size.description', - type: 'string', - defaultValue: '2K', - allowedValues: ['1K', '2K', '4K', '1024x1024', '512x512', '768x768', '1024x768', '768x1024'] - }, - { - name: 'sequential_image_generation', - labelKey: 'params.sequentialGeneration.label', - descriptionKey: 'params.sequentialGeneration.description', - type: 'string', - defaultValue: 'disabled', - allowedValues: ['disabled'] - }, - { - name: 'response_format', - labelKey: 'params.responseFormat.label', - descriptionKey: 'params.responseFormat.description', - type: 'string', - defaultValue: 'b64_json', - allowedValues: ['b64_json', 'url'] - }, - { - name: 'watermark', - labelKey: 'params.watermark.label', - descriptionKey: 'params.watermark.description', - type: 'boolean', - defaultValue: false - } - ] + protected getParameterDefinitions(modelId: string): readonly ImageParameterDefinition[] { + return this.getModelSpec(modelId).parameterDefinitions.map(definition => ({ ...definition })) } - protected getDefaultParameterValues(_modelId: string): Record { - // 所有模型使用统一的默认值 - return { - size: '2K', - sequential_image_generation: 'disabled', - response_format: 'b64_json', - watermark: false - } + protected getDefaultParameterValues(modelId: string): Record { + return { ...this.getModelSpec(modelId).defaultParameterValues } } // public async validateConnection(connectionConfig: Record): Promise { @@ -170,19 +247,8 @@ export class SeedreamImageAdapter extends AbstractImageProviderAdapter { } protected async doGenerate(request: ImageRequest, config: ImageModelConfig): Promise { - // 构建请求体 const overrides: Record = { ...config.paramOverrides, ...request.paramOverrides } - delete overrides.n - delete overrides.batch_size - delete overrides.response_format - const payload: any = { - model: config.modelId, - prompt: request.prompt, - sequential_image_generation: 'disabled', // 固定关闭组图输出,仅返回单张结果 - response_format: 'b64_json', // 强制返回可持久化的 base64,避免 url-only 结果在浏览器侧二次抓取失败 - ...overrides, - n: 1 - } + const payload = this.buildPayload(request, config, overrides) const inputImages = Array.isArray(request.inputImages) ? request.inputImages.filter((image) => typeof image?.b64 === 'string' && image.b64.trim().length > 0) @@ -249,4 +315,70 @@ export class SeedreamImageAdapter extends AbstractImageProviderAdapter { } return await response.json() } + + private getModelSpec(modelId: string): SeedreamModelSpec { + return SEEDREAM_MODEL_SPECS.find(spec => spec.id === modelId) || { + id: modelId, + name: modelId, + description: `Custom Seedream model ${modelId}`, + capabilities: { + text2image: true, + image2image: true, + multiImage: false + }, + parameterDefinitions: [ + { + name: 'size', + labelKey: 'params.size.label', + descriptionKey: 'params.size.description', + type: 'string', + defaultValue: '2K', + allowedValues: [...SEEDREAM_40_SIZE_VALUES] + }, + { + name: 'response_format', + labelKey: 'params.responseFormat.label', + descriptionKey: 'params.responseFormat.description', + type: 'string', + defaultValue: 'b64_json', + allowedValues: ['b64_json', 'url'] + } + ], + defaultParameterValues: { + size: '2K', + response_format: 'b64_json' + } + } + } + + private buildPayload(request: ImageRequest, config: ImageModelConfig, rawOverrides: Record): Record { + const overrides = { ...rawOverrides } + delete overrides.n + delete overrides.batch_size + + if (config.modelId === SEEDREAM_50_LITE_MODEL_ID) { + delete overrides.response_format + delete overrides.sequential_image_generation + delete overrides.watermark + + return { + model: config.modelId, + prompt: request.prompt, + response_format: 'b64_json', + ...overrides, + n: 1 + } + } + + delete overrides.response_format + + return { + model: config.modelId, + prompt: request.prompt, + sequential_image_generation: 'disabled', + response_format: 'b64_json', + ...overrides, + n: 1 + } + } } diff --git a/packages/core/tests/unit/image/image-default-models.test.ts b/packages/core/tests/unit/image/image-default-models.test.ts index 76a62d04..9dd204b5 100644 --- a/packages/core/tests/unit/image/image-default-models.test.ts +++ b/packages/core/tests/unit/image/image-default-models.test.ts @@ -30,6 +30,24 @@ describe('default image models', () => { expect(models['image-seedream'].enabled).toBe(true) }) + it('keeps the builtin image-seedream config pinned to the 4.0 model for compatibility', () => { + process.env.VITE_SEEDREAM_API_KEY = 'seed' + const models = getDefaultImageModels(registry) + + expect(models['image-seedream'].modelId).toBe('doubao-seedream-4-0-250828') + expect(models['image-seedream'].model.name).toBe('Doubao Seedream 4.0') + }) + + it('adds a builtin Seedream 5.0 lite config while keeping the 4.0 config intact', () => { + process.env.VITE_SEEDREAM_API_KEY = 'seed' + const models = getDefaultImageModels(registry) + + expect(models['image-seedream-50-lite']).toBeDefined() + expect(models['image-seedream-50-lite'].providerId).toBe('seedream') + expect(models['image-seedream-50-lite'].modelId).toBe('doubao-seedream-5-0-260128') + expect(models['image-seedream-50-lite'].enabled).toBe(true) + }) + it('includes OpenRouter configuration when API key is present', () => { process.env.VITE_OPENROUTER_API_KEY = 'openrouter-key' const models = getDefaultImageModels(registry) diff --git a/packages/core/tests/unit/image/seedream-adapter.test.ts b/packages/core/tests/unit/image/seedream-adapter.test.ts index aaa14d9c..bcb20e3a 100644 --- a/packages/core/tests/unit/image/seedream-adapter.test.ts +++ b/packages/core/tests/unit/image/seedream-adapter.test.ts @@ -55,6 +55,16 @@ describe('SeedreamImageAdapter', () => { }) }) + test('should include Seedream 4.0, 4.5, and 5.0 lite model ids', () => { + const modelIds = adapter.getModels().map(model => model.id) + + expect(modelIds).toEqual(expect.arrayContaining([ + 'doubao-seedream-4-0-250828', + 'doubao-seedream-4-5-251128', + 'doubao-seedream-5-0-260128' + ])) + }) + test('should declare multi-image capability for Seedream 4.0', () => { const model = adapter.getModels()[0] @@ -62,6 +72,14 @@ describe('SeedreamImageAdapter', () => { expect(model.capabilities.multiImage).toBe(true) }) + test('should declare multi-image capability for Seedream 4.5', () => { + const model = adapter.getModels().find(item => item.id === 'doubao-seedream-4-5-251128') + + expect(model).toBeDefined() + expect(model?.capabilities.image2image).toBe(true) + expect(model?.capabilities.multiImage).toBe(true) + }) + test('should include watermark and size parameters', () => { const models = adapter.getModels() const model = models[0] @@ -318,6 +336,49 @@ describe('SeedreamImageAdapter', () => { expect(body.response_format).toBe('b64_json') }) + + test('should send Seedream 5.0 lite specific payload fields', async () => { + const config: ImageModelConfig = { + id: 'seedream-50-config', + name: 'Seedream 5.0 Config', + providerId: 'seedream', + modelId: 'doubao-seedream-5-0-260128', + enabled: true, + connectionConfig: { + apiKey: 'test-api-key', + baseURL: 'https://ark.cn-beijing.volces.com/api/v3' + }, + paramOverrides: { + output_format: 'jpeg', + tools: ['web_search'], + size: '2048x2048' + } + } + + const request: ImageRequest = { + prompt: '结合联网信息生成新闻插画', + configId: config.id, + count: 1 + } + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + code: 0, + data: [{ b64_json: 'aGVsbG8=' }] + }) + }) + + await adapter.generate(request, config) + + const [, init] = vi.mocked(fetch).mock.calls[0]! + const body = JSON.parse(String(init?.body || '{}')) as Record + + expect(body.output_format).toBe('jpeg') + expect(body.tools).toEqual(['web_search']) + expect(body.response_format).toBe('b64_json') + expect(body.sequential_image_generation).toBeUndefined() + }) }) describe('Provider Capabilities', () => { @@ -348,6 +409,25 @@ describe('SeedreamImageAdapter', () => { expect(sizeParam?.allowedValues).toContain('512x512') }) + test('should use the documented size schema for Seedream 4.5', () => { + const model = adapter.getModels().find(item => item.id === 'doubao-seedream-4-5-251128') + const sizeParam = model?.parameterDefinitions?.find(p => p.name === 'size') + + expect(sizeParam?.defaultValue).toBe('2048x2048') + expect(sizeParam?.allowedValues).toEqual(expect.arrayContaining(['2K', '4K', '2048x2048'])) + expect(sizeParam?.allowedValues).not.toContain('1K') + expect(sizeParam?.allowedValues).not.toContain('512x512') + }) + + test('should expose Seedream 5.0 lite specific parameters', () => { + const model = adapter.getModels().find(item => item.id === 'doubao-seedream-5-0-260128') + const parameterNames = model?.parameterDefinitions.map(p => p.name) || [] + + expect(parameterNames).toEqual(expect.arrayContaining(['size', 'output_format', 'tools'])) + expect(parameterNames).not.toContain('sequential_image_generation') + expect(parameterNames).not.toContain('watermark') + }) + test('should validate watermark parameter', () => { const models = adapter.getModels() const model = models[0] diff --git a/packages/ui/src/i18n/locales/en-US/models.ts b/packages/ui/src/i18n/locales/en-US/models.ts index 703454cc..43fd0539 100644 --- a/packages/ui/src/i18n/locales/en-US/models.ts +++ b/packages/ui/src/i18n/locales/en-US/models.ts @@ -245,6 +245,10 @@ const messages = { "label": "Response Format", "description": "Format of the returned image (URL or Base64)" }, + "outputFormat": { + "label": "Output Format", + "description": "File format for the generated image (such as PNG, JPEG, or WebP)" + }, "watermark": { "label": "Watermark", "description": "Whether to add a watermark to the generated image" @@ -253,6 +257,10 @@ const messages = { "label": "Sequential Generation", "description": "Control sequential image generation mode (for supported models)" }, + "tools": { + "label": "Tools", + "description": "List of 5.0 series extension tools, one tool name per line" + }, "seed": { "label": "Seed", "description": "Random seed for controlling generation results, same seed produces same output" diff --git a/packages/ui/src/i18n/locales/zh-CN/models.ts b/packages/ui/src/i18n/locales/zh-CN/models.ts index d8dfe56a..6fbadf37 100644 --- a/packages/ui/src/i18n/locales/zh-CN/models.ts +++ b/packages/ui/src/i18n/locales/zh-CN/models.ts @@ -245,6 +245,10 @@ const messages = { "label": "响应格式", "description": "返回图片的格式(URL 或 Base64 编码)" }, + "outputFormat": { + "label": "输出格式", + "description": "指定生成图像的文件格式(如 PNG、JPEG 或 WebP)" + }, "watermark": { "label": "水印", "description": "是否在生成的图像上添加水印" @@ -253,6 +257,10 @@ const messages = { "label": "序列生成", "description": "控制序列图像生成模式(支持的模型)" }, + "tools": { + "label": "工具", + "description": "5.0 系列扩展工具列表,每行一个工具名" + }, "seed": { "label": "随机种子", "description": "用于控制生成结果的随机数种子,相同种子产生相同结果" diff --git a/packages/ui/src/i18n/locales/zh-TW/models.ts b/packages/ui/src/i18n/locales/zh-TW/models.ts index ae95e83f..037f1b42 100644 --- a/packages/ui/src/i18n/locales/zh-TW/models.ts +++ b/packages/ui/src/i18n/locales/zh-TW/models.ts @@ -245,6 +245,10 @@ const messages = { "label": "回應格式", "description": "回應圖片的格式(URL 或 Base64 編碼)" }, + "outputFormat": { + "label": "輸出格式", + "description": "指定生成圖像的檔案格式(如 PNG、JPEG 或 WebP)" + }, "watermark": { "label": "浮水印", "description": "是否在生成的圖像上新增浮水印" @@ -253,6 +257,10 @@ const messages = { "label": "序列生成", "description": "控制序列圖像生成模式(支援的模型)" }, + "tools": { + "label": "工具", + "description": "5.0 系列擴充工具列表,每行一個工具名稱" + }, "seed": { "label": "隨機種子", "description": "用於控制生成結果的隨機數種子,相同種子產生相同結果"