@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/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' : null;
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();