@bolloon/bolloon-agent 0.1.30 → 0.1.32
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.
- package/dist/index.js +13 -1
- package/dist/llm/config-store.js +64 -0
- package/dist/llm/pi-ai.js +28 -0
- package/dist/llm/video-config-store.js +171 -0
- package/dist/web/api-config.html +296 -53
- package/dist/web/server.js +55 -0
- package/dist/web/style.css +83 -0
- package/package.json +1 -1
- package/src/index.ts +13 -1
- package/src/llm/audio-config-store.ts +241 -0
- package/src/llm/pi-ai.ts +20 -0
- package/src/llm/video-config-store.ts +251 -0
- package/src/web/api-config.html +296 -53
- package/src/web/server.ts +126 -0
- package/src/web/style.css +83 -0
package/src/index.ts
CHANGED
|
@@ -1499,6 +1499,10 @@ function printHelp(): void {
|
|
|
1499
1499
|
|
|
1500
1500
|
环境变量:
|
|
1501
1501
|
MINIMAX_API_KEY MiniMax API 密钥
|
|
1502
|
+
DEEPSEEK_API_KEY DeepSeek API 密钥
|
|
1503
|
+
KIMI_API_KEY / MOONSHOT_API_KEY Kimi/Moonshot API 密钥
|
|
1504
|
+
GLM_API_KEY / ZHIPU_API_KEY 智谱 GLM API 密钥
|
|
1505
|
+
QWEN_API_KEY / DASHSCOPE_API_KEY 通义千问 API 密钥
|
|
1502
1506
|
OPENAI_API_KEY OpenAI API 密钥(Pi SDK)
|
|
1503
1507
|
ANTHROPIC_API_KEY Anthropic API 密钥(Pi SDK)
|
|
1504
1508
|
PORT Web 服务端口(默认 54188)
|
|
@@ -1563,6 +1567,10 @@ async function main() {
|
|
|
1563
1567
|
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
1564
1568
|
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
1565
1569
|
const hasMinimax = !!process.env.MINIMAX_API_KEY;
|
|
1570
|
+
const hasDeepSeek = !!process.env.DEEPSEEK_API_KEY;
|
|
1571
|
+
const hasKimi = !!(process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY);
|
|
1572
|
+
const hasGlm = !!(process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY);
|
|
1573
|
+
const hasQwen = !!(process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY);
|
|
1566
1574
|
const hasOpenRouter = !!process.env.OPENROUTER_API_KEY;
|
|
1567
1575
|
const hasGemini = !!process.env.GEMINI_API_KEY;
|
|
1568
1576
|
const hasOllama = !!process.env.OLLAMA_BASE_URL;
|
|
@@ -1572,7 +1580,11 @@ async function main() {
|
|
|
1572
1580
|
hasOpenRouter ? 'OpenRouter' :
|
|
1573
1581
|
hasGemini ? 'Gemini' :
|
|
1574
1582
|
hasOllama ? 'Ollama' :
|
|
1575
|
-
hasMinimax ? 'MiniMax' :
|
|
1583
|
+
hasMinimax ? 'MiniMax' :
|
|
1584
|
+
hasDeepSeek ? 'DeepSeek' :
|
|
1585
|
+
hasKimi ? 'Kimi' :
|
|
1586
|
+
hasGlm ? 'GLM' :
|
|
1587
|
+
hasQwen ? 'Qwen' : null;
|
|
1576
1588
|
|
|
1577
1589
|
if (llmProvider) {
|
|
1578
1590
|
s.step(0, 4, `LLM: ${llmProvider}`, 'ok');
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
error: `HTTP ${response.status}: ${errorText.substring(0, 200)}`,
|
|
228
|
+
latency
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
} catch (error: any) {
|
|
232
|
+
return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getAllProviderInfo() {
|
|
237
|
+
return AUDIO_PROVIDER_INFO;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const audioConfigStore = new AudioConfigStore();
|
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
|
|
|
@@ -169,6 +177,10 @@ export class PiAIModel {
|
|
|
169
177
|
openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
|
|
170
178
|
gemini: this.config.model || 'gemini-2.0-flash',
|
|
171
179
|
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
|
|
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
|
}
|
|
@@ -467,6 +483,10 @@ function detectModel(provider: ModelProvider): string {
|
|
|
467
483
|
openrouter: 'anthropic/claude-3.5-sonnet',
|
|
468
484
|
gemini: 'gemini-2.0-flash',
|
|
469
485
|
minimax: 'MiniMax-M2.7',
|
|
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,251 @@
|
|
|
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
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: `HTTP ${response.status}: ${errorText.substring(0, 200)}`,
|
|
238
|
+
latency
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
} catch (error: any) {
|
|
242
|
+
return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getAllProviderInfo() {
|
|
247
|
+
return VIDEO_PROVIDER_INFO;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const videoConfigStore = new VideoConfigStore();
|