@bolloon/bolloon-agent 0.1.30 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Audio Generation API Configuration Store
3
+ *
4
+ * 音频模型配置:MiniMax 提供的 Speech(TTS+ASR)与 Music(文生音乐)。
5
+ * 与 LLM / 视频配置完全独立,持久化到 ~/.bolloon/audio-config.json。
6
+ *
7
+ * 复用 LLM 那一套 MINIMAX_API_KEY 即可(同源)。
8
+ * - TTS: POST /audio/speech (OpenAI 兼容,body 含 model/voice/input)
9
+ * - ASR: POST /audio/transcriptions
10
+ * - Music: POST /music_generation (MiniMax 自有端点)
11
+ */
12
+
13
+ import * as fs from 'fs/promises';
14
+ import * as path from 'path';
15
+
16
+ export type AudioProvider = 'minimax-speech' | 'minimax-music';
17
+
18
+ export interface AudioProviderConfig {
19
+ enabled: boolean;
20
+ apiKey: string;
21
+ baseUrl: string;
22
+ model: string;
23
+ /** TTS 音色:male / female / ... */
24
+ voice?: string;
25
+ /** TTS 语速:0.5-2.0 */
26
+ speed?: number;
27
+ /** TTS 输出格式:mp3 / pcm / wav */
28
+ format?: string;
29
+ /** 音乐生成:instrumental / lyrics */
30
+ mode?: string;
31
+ /** 默认时长(秒) */
32
+ duration?: number;
33
+ requiresApiKey?: boolean;
34
+ }
35
+
36
+ export interface AudioConfig {
37
+ activeProvider: AudioProvider;
38
+ providers: Record<AudioProvider, AudioProviderConfig>;
39
+ updatedAt: string;
40
+ }
41
+
42
+ const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.bolloon');
43
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'audio-config.json');
44
+
45
+ export const DEFAULT_AUDIO_PROVIDER_CONFIGS: Record<AudioProvider, AudioProviderConfig> = {
46
+ 'minimax-speech': {
47
+ enabled: false,
48
+ apiKey: '',
49
+ baseUrl: 'https://api.minimaxi.com/v1',
50
+ model: 'speech-01',
51
+ voice: 'male-qn-jingying',
52
+ speed: 1.0,
53
+ format: 'mp3',
54
+ requiresApiKey: true
55
+ },
56
+ 'minimax-music': {
57
+ enabled: false,
58
+ apiKey: '',
59
+ baseUrl: 'https://api.minimaxi.com/v1',
60
+ model: 'music-01',
61
+ mode: 'instrumental',
62
+ duration: 30,
63
+ requiresApiKey: true
64
+ }
65
+ };
66
+
67
+ export const AUDIO_PROVIDER_INFO: Record<AudioProvider, { name: string; description: string; requiresApiKey: boolean; docs?: string; kind: 'speech' | 'music' }> = {
68
+ 'minimax-speech': {
69
+ name: 'MiniMax Speech',
70
+ description: 'TTS 文生语音 / ASR 语音转写',
71
+ requiresApiKey: true,
72
+ docs: 'https://platform.minimaxi.com/document/T2A%20V2',
73
+ kind: 'speech'
74
+ },
75
+ 'minimax-music': {
76
+ name: 'MiniMax Music',
77
+ description: '文生音乐 (纯音乐 / 带歌词)',
78
+ requiresApiKey: true,
79
+ docs: 'https://platform.minimaxi.com/document/Music%20Generation',
80
+ kind: 'music'
81
+ }
82
+ };
83
+
84
+ function getDefaultConfig(): AudioConfig {
85
+ const envConfigs: Partial<Record<AudioProvider, AudioProviderConfig>> = {};
86
+
87
+ const sharedKey = process.env.MINIMAX_API_KEY || '';
88
+ if (sharedKey) {
89
+ envConfigs['minimax-speech'] = {
90
+ ...DEFAULT_AUDIO_PROVIDER_CONFIGS['minimax-speech'],
91
+ enabled: true,
92
+ apiKey: sharedKey
93
+ };
94
+ envConfigs['minimax-music'] = {
95
+ ...DEFAULT_AUDIO_PROVIDER_CONFIGS['minimax-music'],
96
+ enabled: true,
97
+ apiKey: sharedKey
98
+ };
99
+ }
100
+
101
+ const activeProvider: AudioProvider = 'minimax-speech';
102
+
103
+ const providers = { ...DEFAULT_AUDIO_PROVIDER_CONFIGS };
104
+ for (const [provider, config] of Object.entries(envConfigs)) {
105
+ if (config) {
106
+ providers[provider as AudioProvider] = config;
107
+ }
108
+ }
109
+
110
+ return {
111
+ activeProvider,
112
+ providers,
113
+ updatedAt: new Date().toISOString()
114
+ };
115
+ }
116
+
117
+ class AudioConfigStore {
118
+ private config: AudioConfig | null = null;
119
+ private initialized: boolean = false;
120
+
121
+ async initialize(): Promise<void> {
122
+ if (this.initialized) return;
123
+
124
+ try {
125
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
126
+ const data = await fs.readFile(CONFIG_PATH, 'utf-8');
127
+ const loadedConfig = JSON.parse(data);
128
+
129
+ // 补齐缺失的供应商
130
+ const defaultProviders = Object.keys(DEFAULT_AUDIO_PROVIDER_CONFIGS) as AudioProvider[];
131
+ for (const provider of defaultProviders) {
132
+ if (!loadedConfig.providers[provider]) {
133
+ loadedConfig.providers[provider] = { ...DEFAULT_AUDIO_PROVIDER_CONFIGS[provider] };
134
+ }
135
+ }
136
+
137
+ const activeProvider = loadedConfig.activeProvider as AudioProvider;
138
+ if (!activeProvider || !DEFAULT_AUDIO_PROVIDER_CONFIGS[activeProvider]) {
139
+ loadedConfig.activeProvider = 'minimax-speech';
140
+ }
141
+
142
+ this.config = loadedConfig;
143
+ } catch {
144
+ this.config = getDefaultConfig();
145
+ await this.save();
146
+ }
147
+
148
+ this.initialized = true;
149
+ }
150
+
151
+ private async save(): Promise<void> {
152
+ if (!this.config) return;
153
+ this.config.updatedAt = new Date().toISOString();
154
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2));
155
+ }
156
+
157
+ async getConfig(): Promise<AudioConfig> {
158
+ await this.initialize();
159
+ return { ...this.config! };
160
+ }
161
+
162
+ async getProvider(provider: AudioProvider): Promise<AudioProviderConfig | null> {
163
+ await this.initialize();
164
+ return this.config?.providers[provider] || null;
165
+ }
166
+
167
+ async getActiveProvider(): Promise<AudioProvider> {
168
+ await this.initialize();
169
+ return this.config?.activeProvider || 'minimax-speech';
170
+ }
171
+
172
+ async getActiveProviderConfig(): Promise<AudioProviderConfig | null> {
173
+ await this.initialize();
174
+ const provider = this.config?.activeProvider;
175
+ if (!provider) return null;
176
+ return this.config?.providers[provider] || null;
177
+ }
178
+
179
+ async setActiveProvider(provider: AudioProvider): Promise<void> {
180
+ await this.initialize();
181
+ if (!this.config?.providers[provider]) {
182
+ throw new Error(`Unknown audio provider: ${provider}`);
183
+ }
184
+ this.config.activeProvider = provider;
185
+ await this.save();
186
+ }
187
+
188
+ async updateProvider(provider: AudioProvider, updates: Partial<AudioProviderConfig>): Promise<void> {
189
+ await this.initialize();
190
+ if (!this.config?.providers[provider]) {
191
+ throw new Error(`Unknown audio provider: ${provider}`);
192
+ }
193
+ this.config.providers[provider] = {
194
+ ...this.config.providers[provider],
195
+ ...updates
196
+ };
197
+ await this.save();
198
+ }
199
+
200
+ /**
201
+ * 测试连接:探测 /models 端点。
202
+ */
203
+ async testProvider(provider: AudioProvider): Promise<{ success: boolean; error?: string; latency?: number }> {
204
+ await this.initialize();
205
+
206
+ const config = this.config?.providers[provider];
207
+ if (!config) return { success: false, error: 'Provider not configured' };
208
+ if (!config.enabled) return { success: false, error: 'Provider is not enabled' };
209
+ if (config.requiresApiKey && !config.apiKey) {
210
+ return { success: false, error: 'API key is required' };
211
+ }
212
+
213
+ const startTime = Date.now();
214
+ try {
215
+ const response = await fetch(`${config.baseUrl.replace(/\/$/, '')}/models`, {
216
+ method: 'GET',
217
+ headers: { 'Authorization': `Bearer ${config.apiKey}` }
218
+ });
219
+
220
+ const latency = Date.now() - startTime;
221
+ if (response.ok) {
222
+ return { success: true, latency };
223
+ } else {
224
+ const errorText = await response.text().catch(() => 'Unknown error');
225
+ const hint = response.status === 401
226
+ ? '(请确认是 MiniMax 的 API Key)'
227
+ : response.status === 404
228
+ ? '(端点不存在 — 请检查 baseUrl)'
229
+ : '';
230
+ return {
231
+ success: false,
232
+ error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
233
+ latency
234
+ };
235
+ }
236
+ } catch (error: any) {
237
+ return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
238
+ }
239
+ }
240
+
241
+ getAllProviderInfo() {
242
+ return AUDIO_PROVIDER_INFO;
243
+ }
244
+ }
245
+
246
+ export const audioConfigStore = new AudioConfigStore();
@@ -77,7 +77,7 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
77
77
  enabled: false,
78
78
  apiKey: '',
79
79
  baseUrl: 'https://api.minimaxi.com/v1',
80
- model: 'MiniMax-M2',
80
+ model: 'MiniMax-M3',
81
81
  temperature: 0.7,
82
82
  maxTokens: 4096,
83
83
  requiresApiKey: true
@@ -129,17 +129,22 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
129
129
  }
130
130
  };
131
131
 
132
- export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: string; requiresApiKey: boolean }> = {
133
- openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true },
134
- anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true },
132
+ export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: string; requiresApiKey: boolean; models?: string[] }> = {
133
+ openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true, models: ['gpt-4', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'] },
134
+ anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true, models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] },
135
135
  openrouter: { name: 'OpenRouter', description: '聚合多个 AI 供应商', requiresApiKey: true },
136
- gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true },
136
+ gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true, models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'] },
137
137
  ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
138
- minimax: { name: 'MiniMax', description: '国产大模型服务', requiresApiKey: true },
139
- deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true },
140
- kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true },
141
- glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true },
142
- qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true },
138
+ minimax: {
139
+ name: 'MiniMax',
140
+ description: '国产大模型服务',
141
+ requiresApiKey: true,
142
+ models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2', 'MiniMax-M2.1-highspeed', 'MiniMax-M2.7-highspeed']
143
+ },
144
+ deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true, models: ['deepseek-chat', 'deepseek-reasoner'] },
145
+ kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true, models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] },
146
+ glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true, models: ['glm-4-flash', 'glm-4', 'glm-4-plus', 'glm-4-air', 'glm-4-airx'] },
147
+ qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen-long'] },
143
148
  local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
144
149
  };
145
150
 
@@ -323,7 +328,12 @@ class LLMConfigStore {
323
328
  return { success: true, latency };
324
329
  } else {
325
330
  const errorText = await response.text().catch(() => 'Unknown error');
326
- return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 100)}`, latency };
331
+ const hint = response.status === 401
332
+ ? '(API Key 无效或不匹配该供应商 — 请检查是否复制完整、有无多余空格)'
333
+ : response.status === 404
334
+ ? '(端点不存在 — 请检查 baseUrl)'
335
+ : '';
336
+ return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`, latency };
327
337
  }
328
338
  } catch (error: any) {
329
339
  return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
package/src/llm/pi-ai.ts CHANGED
@@ -137,6 +137,10 @@ export class PiAIModel {
137
137
  openrouter: process.env.OPENROUTER_API_KEY || '',
138
138
  gemini: process.env.GEMINI_API_KEY || '',
139
139
  minimax: process.env.MINIMAX_API_KEY || '',
140
+ deepseek: process.env.DEEPSEEK_API_KEY || '',
141
+ kimi: process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '',
142
+ glm: process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY || '',
143
+ qwen: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '',
140
144
  local: ''
141
145
  };
142
146
  return envVars[this.provider] || '';
@@ -155,6 +159,10 @@ export class PiAIModel {
155
159
  openrouter: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
156
160
  gemini: 'https://generativelanguage.googleapis.com/v1beta',
157
161
  minimax: process.env.MINIMAX_BASE_URL || 'https://api.minimaxi.com/v1',
162
+ deepseek: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1',
163
+ kimi: process.env.KIMI_BASE_URL || process.env.MOONSHOT_BASE_URL || 'https://api.moonshot.cn/v1',
164
+ glm: process.env.GLM_BASE_URL || process.env.ZHIPU_BASE_URL || 'https://open.bigmodel.cn/api/paas/v4',
165
+ qwen: process.env.QWEN_BASE_URL || process.env.DASHSCOPE_BASE_URL || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
158
166
  local: 'http://localhost:11434'
159
167
  };
160
168
 
@@ -168,7 +176,11 @@ export class PiAIModel {
168
176
  ollama: this.config.model || 'llama3.2',
169
177
  openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
170
178
  gemini: this.config.model || 'gemini-2.0-flash',
171
- minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
179
+ minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
180
+ deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-chat',
181
+ kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'moonshot-v1-8k',
182
+ glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-4-flash',
183
+ qwen: this.config.model || process.env.QWEN_MODEL || process.env.DASHSCOPE_MODEL || 'qwen-plus',
172
184
  local: this.config.model || 'llama3.2'
173
185
  };
174
186
  return modelMap[this.provider];
@@ -455,6 +467,10 @@ function detectProvider(): ModelProvider {
455
467
  if (process.env.GEMINI_API_KEY) return 'gemini';
456
468
  if (process.env.OLLAMA_BASE_URL) return 'ollama';
457
469
  if (process.env.MINIMAX_API_KEY) return 'minimax';
470
+ if (process.env.DEEPSEEK_API_KEY) return 'deepseek';
471
+ if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) return 'kimi';
472
+ if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY) return 'glm';
473
+ if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY) return 'qwen';
458
474
 
459
475
  return 'openai';
460
476
  }
@@ -466,7 +482,11 @@ function detectModel(provider: ModelProvider): string {
466
482
  ollama: 'llama3.2',
467
483
  openrouter: 'anthropic/claude-3.5-sonnet',
468
484
  gemini: 'gemini-2.0-flash',
469
- minimax: 'MiniMax-M2.7',
485
+ minimax: 'MiniMax-M3',
486
+ deepseek: 'deepseek-chat',
487
+ kimi: 'moonshot-v1-8k',
488
+ glm: 'glm-4-flash',
489
+ qwen: 'qwen-plus',
470
490
  local: 'llama3.2'
471
491
  };
472
492
  return defaults[provider];
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Video Generation API Configuration Store
3
+ *
4
+ * 视频生成模型配置(与 LLM 完全独立)。当前内置 Seedance(火山引擎 ARK)。
5
+ * Seedance 任务流: POST /contents/generations/tasks → 轮询 GET /contents/generations/tasks/{id}
6
+ */
7
+
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+
11
+ export type VideoProvider = 'seedance' | 'minimax-video';
12
+
13
+ export interface VideoProviderConfig {
14
+ enabled: boolean;
15
+ apiKey: string;
16
+ baseUrl: string;
17
+ model: string;
18
+ /** 默认分辨率,如 720p / 1080p */
19
+ resolution?: string;
20
+ /** 默认时长(秒) */
21
+ duration?: number;
22
+ /** 默认宽高比,如 16:9 / 9:16 / 1:1 */
23
+ ratio?: string;
24
+ /** 是否需要 API Key(火山方舟需要) */
25
+ requiresApiKey?: boolean;
26
+ }
27
+
28
+ export interface VideoConfig {
29
+ activeProvider: VideoProvider;
30
+ providers: Record<VideoProvider, VideoProviderConfig>;
31
+ updatedAt: string;
32
+ }
33
+
34
+ const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.bolloon');
35
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'video-config.json');
36
+
37
+ export const DEFAULT_VIDEO_PROVIDER_CONFIGS: Record<VideoProvider, VideoProviderConfig> = {
38
+ seedance: {
39
+ enabled: false,
40
+ apiKey: '',
41
+ baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
42
+ // 文生视频 lite 版(也支持图生视频,加 --resolution 等参数)
43
+ model: 'doubao-seedance-1-0-lite-t2v-250428',
44
+ resolution: '720p',
45
+ duration: 5,
46
+ ratio: '16:9',
47
+ requiresApiKey: true
48
+ },
49
+ 'minimax-video': {
50
+ enabled: false,
51
+ apiKey: '',
52
+ baseUrl: 'https://api.minimaxi.com/v1',
53
+ model: 'MiniMax-video-01',
54
+ resolution: '720p',
55
+ duration: 6,
56
+ ratio: '16:9',
57
+ requiresApiKey: true
58
+ }
59
+ };
60
+
61
+ export const VIDEO_PROVIDER_INFO: Record<VideoProvider, { name: string; description: string; requiresApiKey: boolean; docs?: string }> = {
62
+ seedance: {
63
+ name: 'Seedance (火山方舟)',
64
+ description: '字节跳动文生视频 / 图生视频模型',
65
+ requiresApiKey: true,
66
+ docs: 'https://www.volcengine.com/docs/82379'
67
+ },
68
+ 'minimax-video': {
69
+ name: 'MiniMax Video',
70
+ description: 'MiniMax 文生视频 (Video-01)',
71
+ requiresApiKey: true,
72
+ docs: 'https://platform.minimaxi.com/document/Video%20Generation'
73
+ }
74
+ };
75
+
76
+ function getDefaultConfig(): VideoConfig {
77
+ const envConfigs: Partial<Record<VideoProvider, VideoProviderConfig>> = {};
78
+
79
+ if (process.env.SEEDANCE_API_KEY || process.env.ARK_API_KEY) {
80
+ envConfigs.seedance = {
81
+ ...DEFAULT_VIDEO_PROVIDER_CONFIGS.seedance,
82
+ enabled: true,
83
+ apiKey: process.env.SEEDANCE_API_KEY || process.env.ARK_API_KEY || ''
84
+ };
85
+ }
86
+
87
+ const sharedKey = process.env.MINIMAX_API_KEY || '';
88
+ if (sharedKey) {
89
+ envConfigs['minimax-video'] = {
90
+ ...DEFAULT_VIDEO_PROVIDER_CONFIGS['minimax-video'],
91
+ enabled: true,
92
+ apiKey: sharedKey
93
+ };
94
+ }
95
+
96
+ const activeProvider: VideoProvider = 'seedance';
97
+
98
+ const providers = { ...DEFAULT_VIDEO_PROVIDER_CONFIGS };
99
+ for (const [provider, config] of Object.entries(envConfigs)) {
100
+ if (config) {
101
+ providers[provider as VideoProvider] = config;
102
+ }
103
+ }
104
+
105
+ return {
106
+ activeProvider,
107
+ providers,
108
+ updatedAt: new Date().toISOString()
109
+ };
110
+ }
111
+
112
+ class VideoConfigStore {
113
+ private config: VideoConfig | null = null;
114
+ private initialized: boolean = false;
115
+
116
+ async initialize(): Promise<void> {
117
+ if (this.initialized) return;
118
+
119
+ try {
120
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
121
+ const data = await fs.readFile(CONFIG_PATH, 'utf-8');
122
+ const loadedConfig = JSON.parse(data);
123
+
124
+ // 确保加载的配置包含所有默认供应商
125
+ const defaultProviders = Object.keys(DEFAULT_VIDEO_PROVIDER_CONFIGS) as VideoProvider[];
126
+ for (const provider of defaultProviders) {
127
+ if (!loadedConfig.providers[provider]) {
128
+ loadedConfig.providers[provider] = { ...DEFAULT_VIDEO_PROVIDER_CONFIGS[provider] };
129
+ }
130
+ }
131
+
132
+ const activeProvider = loadedConfig.activeProvider as VideoProvider;
133
+ if (!activeProvider || !DEFAULT_VIDEO_PROVIDER_CONFIGS[activeProvider]) {
134
+ loadedConfig.activeProvider = 'seedance';
135
+ }
136
+
137
+ this.config = loadedConfig;
138
+ } catch {
139
+ this.config = getDefaultConfig();
140
+ await this.save();
141
+ }
142
+
143
+ this.initialized = true;
144
+ }
145
+
146
+ private async save(): Promise<void> {
147
+ if (!this.config) return;
148
+ this.config.updatedAt = new Date().toISOString();
149
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2));
150
+ }
151
+
152
+ async getConfig(): Promise<VideoConfig> {
153
+ await this.initialize();
154
+ return { ...this.config! };
155
+ }
156
+
157
+ async getProvider(provider: VideoProvider): Promise<VideoProviderConfig | null> {
158
+ await this.initialize();
159
+ return this.config?.providers[provider] || null;
160
+ }
161
+
162
+ async getActiveProvider(): Promise<VideoProvider> {
163
+ await this.initialize();
164
+ return this.config?.activeProvider || 'seedance';
165
+ }
166
+
167
+ async getActiveProviderConfig(): Promise<VideoProviderConfig | null> {
168
+ await this.initialize();
169
+ const provider = this.config?.activeProvider;
170
+ if (!provider) return null;
171
+ return this.config?.providers[provider] || null;
172
+ }
173
+
174
+ async setActiveProvider(provider: VideoProvider): Promise<void> {
175
+ await this.initialize();
176
+
177
+ if (!this.config?.providers[provider]) {
178
+ throw new Error(`Unknown video provider: ${provider}`);
179
+ }
180
+
181
+ this.config.activeProvider = provider;
182
+ await this.save();
183
+ }
184
+
185
+ async updateProvider(provider: VideoProvider, updates: Partial<VideoProviderConfig>): Promise<void> {
186
+ await this.initialize();
187
+
188
+ if (!this.config?.providers[provider]) {
189
+ throw new Error(`Unknown video provider: ${provider}`);
190
+ }
191
+
192
+ this.config.providers[provider] = {
193
+ ...this.config.providers[provider],
194
+ ...updates
195
+ };
196
+
197
+ await this.save();
198
+ }
199
+
200
+ /**
201
+ * 测试连接:只校验 API key 是否能被 ARK 接受(创建任务失败不算连接失败,
202
+ * 返回 401/403 才算失败)。
203
+ */
204
+ async testProvider(provider: VideoProvider): Promise<{ success: boolean; error?: string; latency?: number }> {
205
+ await this.initialize();
206
+
207
+ const config = this.config?.providers[provider];
208
+ if (!config) {
209
+ return { success: false, error: 'Provider not configured' };
210
+ }
211
+
212
+ if (!config.enabled) {
213
+ return { success: false, error: 'Provider is not enabled' };
214
+ }
215
+
216
+ if (config.requiresApiKey && !config.apiKey) {
217
+ return { success: false, error: 'API key is required' };
218
+ }
219
+
220
+ const startTime = Date.now();
221
+
222
+ try {
223
+ // 列出可用模型(轻量级健康检查)。ARK 端点:GET /models
224
+ const response = await fetch(`${config.baseUrl.replace(/\/$/, '')}/models`, {
225
+ method: 'GET',
226
+ headers: { 'Authorization': `Bearer ${config.apiKey}` }
227
+ });
228
+
229
+ const latency = Date.now() - startTime;
230
+
231
+ if (response.ok) {
232
+ return { success: true, latency };
233
+ } else {
234
+ const errorText = await response.text().catch(() => 'Unknown error');
235
+ // 401 通常是 key 错误,给出针对性提示
236
+ const hint = response.status === 401
237
+ ? '(请确认是火山方舟 ARK 的 API Key,不是 MiniMax / 其他平台)'
238
+ : response.status === 404
239
+ ? '(端点不存在 — 火山方舟可能没有 /models,请检查 baseUrl)'
240
+ : '';
241
+ return {
242
+ success: false,
243
+ error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
244
+ latency
245
+ };
246
+ }
247
+ } catch (error: any) {
248
+ return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
249
+ }
250
+ }
251
+
252
+ getAllProviderInfo() {
253
+ return VIDEO_PROVIDER_INFO;
254
+ }
255
+ }
256
+
257
+ export const videoConfigStore = new VideoConfigStore();