Skip to content

Commit c954ffa

Browse files
committed
feat(core): 内置模型环境变量自动同步
当内置模型之前因无 API Key 而禁用,后续环境变量配置了新 Key 时, 自动注入 apiKey 并启用模型。 - 新增 getBuiltinImageConfigIds() 函数用于判断内置图像模型 - ImageModelManager.init() 添加自动启用逻辑 - ModelManager.init() 添加自动启用逻辑 - 新增 shouldAutoEnableBuiltinModel() 方法 修复 #214
1 parent f337c57 commit c954ffa

File tree

3 files changed

+179
-139
lines changed

3 files changed

+179
-139
lines changed

packages/core/src/services/image-model/defaults.ts

Lines changed: 76 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -2,167 +2,107 @@ import type { ImageModelConfig, IImageAdapterRegistry } from '../image/types'
22
import { ImageAdapterRegistry } from '../image/adapters/registry'
33
import { getEnvVar } from '../../utils/environment'
44

5+
/**
6+
* Provider ID -> 环境变量 key 映射(与文本模型风格一致)
7+
* 新增 Provider 只需在此添加一行
8+
*/
9+
const IMAGE_PROVIDER_ENV_KEYS = {
10+
openrouter: 'VITE_OPENROUTER_API_KEY',
11+
gemini: 'VITE_GEMINI_API_KEY',
12+
openai: 'VITE_OPENAI_API_KEY',
13+
siliconflow: 'VITE_SILICONFLOW_API_KEY',
14+
seedream: 'VITE_SEEDREAM_API_KEY',
15+
dashscope: 'VITE_DASHSCOPE_API_KEY'
16+
} as const
17+
18+
/**
19+
* 配置 ID 映射(保持现有 ID 不变以兼容用户数据)
20+
* name 将从 provider.name 获取,无需硬编码
21+
*/
22+
const IMAGE_CONFIG_IDS: Record<string, string> = {
23+
openrouter: 'image-openrouter-nanobanana',
24+
gemini: 'image-gemini-nanobanana',
25+
openai: 'image-openai-gpt',
26+
siliconflow: 'image-siliconflow-kolors',
27+
seedream: 'image-seedream',
28+
dashscope: 'image-dashscope'
29+
}
30+
31+
/**
32+
* 特殊 baseURL 环境变量(仅需要覆盖的 Provider)
33+
*/
34+
const IMAGE_BASE_URL_ENV_KEYS: Record<string, string> = {
35+
openai: 'VITE_OPENAI_BASE_URL',
36+
seedream: 'VITE_SEEDREAM_BASE_URL'
37+
}
38+
539
/**
640
* 图像模型默认配置生成器
7-
* 返回完整的自包含配置对象,包含provider和model完整信息
41+
* 返回完整的自包含配置对象,包含 provider 和 model 完整信息
842
*
9-
* baseURL 和默认 modelId 从 Adapter 获取,避免硬编码
43+
* 使用 Provider-Adapter 架构生成完整的元数据,
44+
* 所有配置信息(Provider ID、名称、BaseURL、默认模型、参数)均从 Adapter 获取。
1045
*
1146
* @param registry 可选,图像适配器注册表(用于依赖注入和测试)
1247
*/
1348
export function getDefaultImageModels(registry?: IImageAdapterRegistry): Record<string, ImageModelConfig> {
1449
const adapterRegistry = registry || new ImageAdapterRegistry()
50+
const result: Record<string, ImageModelConfig> = {}
1551

16-
// 环境变量
17-
const GEMINI_API_KEY = getEnvVar('VITE_GEMINI_API_KEY').trim()
18-
const SEEDREAM_API_KEY = (
19-
getEnvVar('VITE_SEEDREAM_API_KEY') ||
20-
getEnvVar('VITE_ARK_API_KEY')).trim()
21-
const SEEDREAM_BASE_URL = (
22-
getEnvVar('VITE_SEEDREAM_BASE_URL') ||
23-
getEnvVar('VITE_ARK_BASE_URL')).trim()
24-
const OPENROUTER_API_KEY = getEnvVar('VITE_OPENROUTER_API_KEY').trim()
25-
const SILICONFLOW_API_KEY = getEnvVar('VITE_SILICONFLOW_API_KEY').trim()
26-
const OPENAI_API_KEY = getEnvVar('VITE_OPENAI_API_KEY').trim()
27-
const OPENAI_BASE_URL = getEnvVar('VITE_OPENAI_BASE_URL').trim()
28-
const DASHSCOPE_API_KEY = getEnvVar('VITE_DASHSCOPE_API_KEY').trim()
52+
// 批量生成配置(与文本模型风格一致)
53+
for (const [providerId, envKey] of Object.entries(IMAGE_PROVIDER_ENV_KEYS)) {
54+
const configId = IMAGE_CONFIG_IDS[providerId]
55+
if (!configId) continue
2956

30-
// 辅助函数:获取 Provider 的默认 baseURL
31-
const getDefaultBaseURL = (providerId: string): string => {
32-
const adapter = adapterRegistry.getAdapter(providerId)
33-
return adapter.getProvider().defaultBaseURL || ''
34-
}
35-
36-
// 辅助函数:获取 Provider 的第一个模型 ID 作为默认
37-
const getDefaultModelId = (providerId: string): string => {
38-
const models = adapterRegistry.getStaticModels(providerId)
39-
return models[0]?.id || providerId
40-
}
41-
42-
// 辅助函数:构建完整配置
43-
const buildConfig = (
44-
configId: string,
45-
name: string,
46-
providerId: string,
47-
modelId: string,
48-
enabled: boolean,
49-
connectionConfig?: any,
50-
paramOverrides?: any
51-
): ImageModelConfig => {
5257
const adapter = adapterRegistry.getAdapter(providerId)
5358
const provider = adapter.getProvider()
59+
const models = adapterRegistry.getStaticModels(providerId)
60+
const defaultModel = models[0] || adapter.buildDefaultModel(providerId)
5461

55-
// 尝试从静态模型列表获取模型信息
56-
let model = adapterRegistry.getStaticModels(providerId).find(m => m.id === modelId)
62+
// 获取 API Key(Seedream 支持备选环境变量)
63+
let apiKey = getEnvVar(envKey).trim()
64+
if (!apiKey && providerId === 'seedream') {
65+
apiKey = getEnvVar('VITE_ARK_API_KEY').trim()
66+
}
5767

58-
// 如果静态模型不存在,使用buildDefaultModel构建
59-
if (!model) {
60-
model = adapter.buildDefaultModel(modelId)
68+
// 获取 baseURL(支持环境变量覆盖)
69+
let baseURL = provider.defaultBaseURL || ''
70+
const baseURLEnvKey = IMAGE_BASE_URL_ENV_KEYS[providerId]
71+
if (baseURLEnvKey) {
72+
let envBaseURL = getEnvVar(baseURLEnvKey).trim()
73+
// Seedream 备选
74+
if (!envBaseURL && providerId === 'seedream') {
75+
envBaseURL = getEnvVar('VITE_ARK_BASE_URL').trim()
76+
}
77+
if (envBaseURL) baseURL = envBaseURL
6178
}
6279

63-
// 合并模型默认参数和用户指定的参数覆盖
64-
// 用户指定的参数优先级更高
65-
const modelDefaults = model.defaultParameterValues || {}
66-
const mergedParamOverrides = { ...modelDefaults, ...(paramOverrides ?? {}) }
80+
// 直接从模型获取默认参数值(与文本模型一致)
81+
const defaultParamValues = defaultModel.defaultParameterValues || {}
6782

68-
return {
83+
result[configId] = {
6984
id: configId,
70-
name,
85+
name: provider.name, // 从 provider 获取名称,不再硬编码
7186
providerId,
72-
modelId,
73-
enabled,
74-
connectionConfig,
75-
paramOverrides: mergedParamOverrides,
87+
modelId: defaultModel.id,
88+
enabled: !!apiKey,
89+
connectionConfig: { apiKey, baseURL },
90+
paramOverrides: { ...defaultParamValues },
7691
customParamOverrides: {},
7792
provider,
78-
model
93+
model: defaultModel
7994
}
8095
}
8196

82-
// 预设配置列表
83-
// baseURL 优先使用环境变量,否则从 Provider 获取默认值
84-
// modelId 使用 Provider 的第一个模型作为默认
85-
const entries = [
86-
{
87-
id: 'image-openrouter-nanobanana',
88-
name: 'OpenRouter Nano Banana',
89-
providerId: 'openrouter',
90-
modelId: getDefaultModelId('openrouter'),
91-
enabled: !!OPENROUTER_API_KEY,
92-
connectionConfig: {
93-
apiKey: OPENROUTER_API_KEY,
94-
baseURL: getDefaultBaseURL('openrouter')
95-
},
96-
paramOverrides: {}
97-
},
98-
{
99-
id: 'image-gemini-nanobanana',
100-
name: 'Gemini Nano Banana',
101-
providerId: 'gemini',
102-
modelId: getDefaultModelId('gemini'),
103-
enabled: !!GEMINI_API_KEY,
104-
connectionConfig: {
105-
apiKey: GEMINI_API_KEY,
106-
baseURL: getDefaultBaseURL('gemini')
107-
},
108-
paramOverrides: { outputMimeType: 'image/png' }
109-
},
110-
{
111-
id: 'image-openai-gpt',
112-
name: 'OpenAI GPT Image 1',
113-
providerId: 'openai',
114-
modelId: getDefaultModelId('openai'),
115-
enabled: !!OPENAI_API_KEY,
116-
connectionConfig: {
117-
apiKey: OPENAI_API_KEY,
118-
baseURL: OPENAI_BASE_URL || getDefaultBaseURL('openai')
119-
},
120-
paramOverrides: { size: '1024x1024', quality: 'auto', background: 'auto' }
121-
},
122-
{
123-
id: 'image-siliconflow-kolors',
124-
name: 'SiliconFlow Kolors',
125-
providerId: 'siliconflow',
126-
modelId: getDefaultModelId('siliconflow'),
127-
enabled: !!SILICONFLOW_API_KEY,
128-
connectionConfig: {
129-
apiKey: SILICONFLOW_API_KEY,
130-
baseURL: getDefaultBaseURL('siliconflow')
131-
},
132-
paramOverrides: { image_size: '1024x1024', num_inference_steps: 20, guidance_scale: 7.5, outputMimeType: 'image/png' }
133-
},
134-
{
135-
id: 'image-seedream',
136-
name: 'Doubao Seedream 4.0',
137-
providerId: 'seedream',
138-
modelId: getDefaultModelId('seedream'),
139-
enabled: !!SEEDREAM_API_KEY,
140-
connectionConfig: {
141-
apiKey: SEEDREAM_API_KEY,
142-
baseURL: SEEDREAM_BASE_URL || getDefaultBaseURL('seedream')
143-
},
144-
paramOverrides: { size: '1024x1024', watermark: false, outputMimeType: 'image/png' }
145-
},
146-
{
147-
id: 'image-dashscope',
148-
name: 'DashScope Qwen',
149-
providerId: 'dashscope',
150-
modelId: getDefaultModelId('dashscope'),
151-
enabled: !!DASHSCOPE_API_KEY,
152-
connectionConfig: {
153-
apiKey: DASHSCOPE_API_KEY,
154-
baseURL: getDefaultBaseURL('dashscope')
155-
},
156-
paramOverrides: {}
157-
}
158-
]
159-
160-
const models: Record<string, ImageModelConfig> = {}
161-
for (const e of entries) {
162-
models[e.id] = buildConfig(e.id, e.name, e.providerId, e.modelId, e.enabled, e.connectionConfig, e.paramOverrides)
163-
}
97+
return result
98+
}
16499

165-
return models
100+
/**
101+
* 获取所有内置图像模型配置的 ID 列表
102+
* 用于判断某个配置是否为内置模型(而非用户自定义)
103+
*/
104+
export function getBuiltinImageConfigIds(): string[] {
105+
return Object.values(IMAGE_CONFIG_IDS)
166106
}
167107

168108
// 直接导出所有图像模型配置(保持向后兼容,与文本模型风格一致)

packages/core/src/services/image-model/manager.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IStorageProvider } from '../storage/types'
77
import { StorageAdapter } from '../storage/adapter'
88
import { CORE_SERVICE_KEYS } from '../../constants/storage-keys'
99
import { ImportExportError } from '../../interfaces/import-export'
10-
import { getDefaultImageModels } from './defaults'
10+
import { getDefaultImageModels, getBuiltinImageConfigIds } from './defaults'
1111

1212
/**
1313
* 图像模型管理器:专注于配置管理,遵循新的三层架构
@@ -65,11 +65,27 @@ export class ImageModelManager implements IImageModelManager {
6565
}
6666
}
6767
const defaults = getDefaultImageModels(this.registry)
68-
// 合并默认项
68+
// 合并默认项,并检查是否需要自动启用内置模型
6969
for (const [key, cfg] of Object.entries(defaults)) {
7070
if (!data[key]) {
71+
// 添加缺失的默认模型
7172
data[key] = cfg
7273
changed = true
74+
} else {
75+
// 检查是否需要自动注入 apiKey 并启用内置模型
76+
const existingConfig = data[key]
77+
if (this.shouldAutoEnableBuiltinModel(key, existingConfig, cfg)) {
78+
data[key] = {
79+
...existingConfig,
80+
connectionConfig: {
81+
...(existingConfig.connectionConfig || {}),
82+
apiKey: cfg.connectionConfig?.apiKey
83+
},
84+
enabled: true
85+
}
86+
changed = true
87+
console.log(`[ImageModelManager] Auto-enabled builtin model with new API key: ${key}`)
88+
}
7389
}
7490
}
7591

@@ -373,6 +389,41 @@ export class ImageModelManager implements IImageModelManager {
373389
}
374390
}
375391

392+
/**
393+
* 判断是否应该自动启用内置模型
394+
* 条件:内置模型 + 存储的 apiKey 为空 + enabled 为 false + 新配置有 apiKey
395+
*/
396+
private shouldAutoEnableBuiltinModel(
397+
configId: string,
398+
storedConfig: ImageModelConfig,
399+
defaultConfig: ImageModelConfig
400+
): boolean {
401+
// 1. 必须是内置模型
402+
const builtinIds = getBuiltinImageConfigIds()
403+
if (!builtinIds.includes(configId)) {
404+
return false
405+
}
406+
407+
// 2. 存储的配置必须是禁用状态
408+
if (storedConfig.enabled !== false) {
409+
return false
410+
}
411+
412+
// 3. 存储的 apiKey 必须为空
413+
const storedApiKey = storedConfig.connectionConfig?.apiKey?.trim() || ''
414+
if (storedApiKey !== '') {
415+
return false
416+
}
417+
418+
// 4. 新的默认配置必须有 apiKey
419+
const newApiKey = defaultConfig.connectionConfig?.apiKey?.trim() || ''
420+
if (newApiKey === '') {
421+
return false
422+
}
423+
424+
return true
425+
}
426+
376427
private validateConfig(config: ImageModelConfig): void {
377428
const errors: string[] = []
378429

packages/core/src/services/model/manager.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IModelManager, ModelConfig, TextModelConfig } from './types';
22
import { IStorageProvider } from '../storage/types';
33
import { StorageAdapter } from '../storage/adapter';
4-
import { getAllModels } from './defaults';
4+
import { getAllModels, getBuiltinModelIds } from './defaults';
55
import { ModelConfigError } from '../llm/errors';
66
import { validateOverrides } from './parameter-utils';
77
import { ElectronConfigManager, isElectronRenderer } from './electron-config';
@@ -126,6 +126,20 @@ export class ModelManager implements IModelManager {
126126
hasUpdates = true;
127127
console.log(`[ModelManager] Patched missing metadata for model: ${key}`);
128128
}
129+
130+
// 检查是否需要自动注入 apiKey 并启用内置模型
131+
if (this.shouldAutoEnableBuiltinModel(key, updatedModel, defaultConfig)) {
132+
updatedModels[key] = {
133+
...updatedModel,
134+
connectionConfig: {
135+
...(updatedModel.connectionConfig || {}),
136+
apiKey: defaultConfig.connectionConfig?.apiKey
137+
},
138+
enabled: true
139+
};
140+
hasUpdates = true;
141+
console.log(`[ModelManager] Auto-enabled builtin model with new API key: ${key}`);
142+
}
129143
} else if (isLegacyConfig(existingModel)) {
130144
// 旧格式,尝试使用 Registry 转换为新格式
131145
try {
@@ -507,6 +521,41 @@ export class ModelManager implements IModelManager {
507521
);
508522
}
509523

524+
/**
525+
* 判断是否应该自动启用内置模型
526+
* 条件:内置模型 + 存储的 apiKey 为空 + enabled 为 false + 新配置有 apiKey
527+
*/
528+
private shouldAutoEnableBuiltinModel(
529+
modelId: string,
530+
storedConfig: TextModelConfig,
531+
defaultConfig: TextModelConfig
532+
): boolean {
533+
// 1. 必须是内置模型
534+
const builtinIds = getBuiltinModelIds();
535+
if (!builtinIds.includes(modelId)) {
536+
return false;
537+
}
538+
539+
// 2. 存储的配置必须是禁用状态
540+
if (storedConfig.enabled !== false) {
541+
return false;
542+
}
543+
544+
// 3. 存储的 apiKey 必须为空
545+
const storedApiKey = storedConfig.connectionConfig?.apiKey?.trim() || '';
546+
if (storedApiKey !== '') {
547+
return false;
548+
}
549+
550+
// 4. 新的默认配置必须有 apiKey
551+
const newApiKey = defaultConfig.connectionConfig?.apiKey?.trim() || '';
552+
if (newApiKey === '') {
553+
return false;
554+
}
555+
556+
return true;
557+
}
558+
510559
/**
511560
* 验证 TextModelConfig 配置
512561
*/

0 commit comments

Comments
 (0)