@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.
@@ -20,9 +20,47 @@
20
20
  <a href="/" class="back-link">← 返回主页</a>
21
21
  </div>
22
22
 
23
- <!-- 提供商列表 -->
24
- <div class="provider-list" id="providerList">
25
- <div class="loading-state">加载中...</div>
23
+ <!-- 标签切换 -->
24
+ <div class="api-tabs">
25
+ <button class="api-tab active" data-tab="llm" onclick="switchTab('llm')">
26
+ <span class="api-tab-icon">💬</span>
27
+ <span>LLM 对话</span>
28
+ </button>
29
+ <button class="api-tab" data-tab="video" onclick="switchTab('video')">
30
+ <span class="api-tab-icon">🎬</span>
31
+ <span>视频生成</span>
32
+ </button>
33
+ <button class="api-tab" data-tab="audio" onclick="switchTab('audio')">
34
+ <span class="api-tab-icon">🎵</span>
35
+ <span>音频生成</span>
36
+ </button>
37
+ </div>
38
+
39
+ <!-- LLM 面板 -->
40
+ <div class="api-panel" id="panel-llm" data-panel="llm">
41
+ <div class="provider-list" id="providerList">
42
+ <div class="loading-state">加载中...</div>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- 视频生成面板 -->
47
+ <div class="api-panel" id="panel-video" data-panel="video" style="display: none;">
48
+ <div class="video-intro">
49
+ <p>视频生成走异步任务流(提交任务 → 轮询结果),与 LLM 实时对话不同。Seedance 由字节火山方舟 (ARK) 提供,支持文生视频 (t2v) 与图生视频 (i2v);MiniMax Video 为同 tab 下的另一选项。</p>
50
+ </div>
51
+ <div class="provider-list" id="videoProviderList">
52
+ <div class="loading-state">加载中...</div>
53
+ </div>
54
+ </div>
55
+
56
+ <!-- 音频生成面板 -->
57
+ <div class="api-panel" id="panel-audio" data-panel="audio" style="display: none;">
58
+ <div class="video-intro">
59
+ <p>音频生成:TTS(文生语音)/ Music(文生音乐)。MiniMax 提供 speech-01 与 music-01 模型,TTS 默认走 OpenAI 兼容 <code>/audio/speech</code> 端点,Music 走 MiniMax 自有 <code>/music_generation</code> 端点。</p>
60
+ </div>
61
+ <div class="provider-list" id="audioProviderList">
62
+ <div class="loading-state">加载中...</div>
63
+ </div>
26
64
  </div>
27
65
 
28
66
  <!-- 配置弹窗 -->
@@ -43,7 +81,7 @@
43
81
  <div class="form-group">
44
82
  <label>API Key</label>
45
83
  <input type="password" id="apiKeyInput" placeholder="输入 API Key">
46
- <p class="form-hint">已有 Key 会保留,输入新值以更新</p>
84
+ <p class="form-hint" id="apiKeyHint">已有 Key 会保留,输入新值以更新</p>
47
85
  </div>
48
86
 
49
87
  <div class="form-group">
@@ -52,11 +90,80 @@
52
90
  <p class="form-hint">留空使用默认地址</p>
53
91
  </div>
54
92
 
55
- <div class="form-row">
93
+ <div class="form-group">
94
+ <label>模型</label>
95
+ <input type="text" id="modelInput" placeholder="如 gpt-4" list="modelSuggestList">
96
+ <datalist id="modelSuggestList"></datalist>
97
+ <p class="form-hint" id="modelHint"></p>
98
+ </div>
99
+
100
+ <!-- 视频模型专用字段 -->
101
+ <div class="video-only-fields" style="display: none;">
102
+ <div class="form-row">
103
+ <div class="form-group">
104
+ <label>分辨率</label>
105
+ <select id="resolutionInput">
106
+ <option value="480p">480p</option>
107
+ <option value="720p">720p</option>
108
+ <option value="1080p">1080p</option>
109
+ </select>
110
+ </div>
111
+ <div class="form-group">
112
+ <label>宽高比</label>
113
+ <select id="ratioInput">
114
+ <option value="16:9">16:9</option>
115
+ <option value="9:16">9:16</option>
116
+ <option value="1:1">1:1</option>
117
+ <option value="4:3">4:3</option>
118
+ <option value="3:4">3:4</option>
119
+ <option value="21:9">21:9</option>
120
+ </select>
121
+ </div>
122
+ </div>
56
123
  <div class="form-group">
57
- <label>模型</label>
58
- <input type="text" id="modelInput" placeholder=" gpt-4">
124
+ <label>默认时长(秒)</label>
125
+ <input type="number" id="durationInput" min="3" max="12" step="1" value="5">
126
+ <p class="form-hint">Seedance 支持 3-12 秒</p>
59
127
  </div>
128
+ </div>
129
+
130
+ <!-- 音频模型专用字段 -->
131
+ <div class="audio-only-fields" style="display: none;">
132
+ <div class="form-group">
133
+ <label>音色 / 风格</label>
134
+ <input type="text" id="voiceInput" placeholder="male-qn-jingying / female-shaonv ...">
135
+ <p class="form-hint" id="voiceHint"></p>
136
+ </div>
137
+ <div class="form-row">
138
+ <div class="form-group">
139
+ <label>语速</label>
140
+ <input type="number" id="speedInput" min="0.5" max="2.0" step="0.1" value="1.0">
141
+ </div>
142
+ <div class="form-group">
143
+ <label>输出格式</label>
144
+ <select id="formatInput">
145
+ <option value="mp3">mp3</option>
146
+ <option value="pcm">pcm</option>
147
+ <option value="wav">wav</option>
148
+ </select>
149
+ </div>
150
+ </div>
151
+ <div class="form-group" id="audioModeGroup" style="display: none;">
152
+ <label>模式</label>
153
+ <select id="modeInput">
154
+ <option value="instrumental">纯音乐</option>
155
+ <option value="lyrics">带歌词</option>
156
+ </select>
157
+ </div>
158
+ <div class="form-group" id="audioDurationGroup" style="display: none;">
159
+ <label>默认时长(秒)</label>
160
+ <input type="number" id="audioDurationInput" min="5" max="120" step="1" value="30">
161
+ <p class="form-hint">music-01 支持 5-120 秒</p>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- LLM 专用字段 -->
166
+ <div class="llm-only-fields">
60
167
  <div class="form-group form-group-small">
61
168
  <label>温度</label>
62
169
  <input type="number" id="temperatureInput" value="0.7" min="0" max="2" step="0.1">
@@ -80,49 +187,115 @@
80
187
  </div>
81
188
 
82
189
  <script>
83
- // 当前配置数据
84
- let configData = null;
190
+ // 当前 tab
191
+ let currentTab = 'llm';
192
+
193
+ // 缓存三份配置
194
+ let llmConfigData = null;
195
+ let videoConfigData = null;
196
+ let audioConfigData = null;
85
197
  let currentProvider = null;
198
+ let currentProviderType = 'llm'; // 'llm' | 'video' | 'audio'
199
+
200
+ // ==================== Tab 切换 ====================
201
+ function switchTab(tab) {
202
+ currentTab = tab;
203
+ document.querySelectorAll('.api-tab').forEach(btn => {
204
+ btn.classList.toggle('active', btn.dataset.tab === tab);
205
+ });
206
+ document.querySelectorAll('.api-panel').forEach(panel => {
207
+ panel.style.display = panel.dataset.panel === tab ? 'block' : 'none';
208
+ });
209
+ // 切换 tab 时更新计数 badge
210
+ updateCountBadge();
211
+ }
212
+
213
+ function updateCountBadge() {
214
+ const data = currentTab === 'llm' ? llmConfigData :
215
+ currentTab === 'video' ? videoConfigData : audioConfigData;
216
+ if (!data) {
217
+ document.getElementById('configCount').textContent = '加载中...';
218
+ return;
219
+ }
220
+ let configured = 0, total = 0;
221
+ for (const key in data.providers) {
222
+ total++;
223
+ const p = data.providers[key];
224
+ if (currentTab === 'llm') {
225
+ if (p.enabled && p.apiKey) configured++;
226
+ } else {
227
+ const info = data.providerInfo[key] || {};
228
+ if (p.enabled && (!info.requiresApiKey || p.apiKey)) configured++;
229
+ }
230
+ }
231
+ const labelMap = { llm: '', video: '视频', audio: '音频' };
232
+ const label = labelMap[currentTab] || '';
233
+ document.getElementById('configCount').textContent = configured + '/' + total + ' ' + label + '已配置';
234
+ }
235
+
236
+ // ==================== 加载 ====================
237
+ async function loadAll() {
238
+ await Promise.all([loadLLMConfig(), loadVideoConfig(), loadAudioConfig()]);
239
+ }
86
240
 
87
- // 加载配置
88
- async function loadConfig() {
241
+ async function loadLLMConfig() {
89
242
  try {
90
243
  const resp = await fetch('/api/llm-config');
91
- configData = await resp.json();
92
- renderProviders();
244
+ llmConfigData = await resp.json();
245
+ renderProviders('llm');
246
+ if (currentTab === 'llm') updateCountBadge();
93
247
  } catch (err) {
94
248
  document.getElementById('providerList').innerHTML =
95
249
  '<div class="error-state">加载失败: ' + err.message + '</div>';
96
250
  }
97
251
  }
98
252
 
99
- // 渲染提供商列表
100
- function renderProviders() {
101
- const list = document.getElementById('providerList');
102
- const providers = configData.providers;
103
- const info = configData.providerInfo;
253
+ async function loadVideoConfig() {
254
+ try {
255
+ const resp = await fetch('/api/video-config');
256
+ videoConfigData = await resp.json();
257
+ renderProviders('video');
258
+ if (currentTab === 'video') updateCountBadge();
259
+ } catch (err) {
260
+ document.getElementById('videoProviderList').innerHTML =
261
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
262
+ }
263
+ }
104
264
 
105
- // 计算已配置数量
106
- let configured = 0;
107
- let total = 0;
108
- for (const key in providers) {
109
- total++;
110
- if (providers[key].enabled && providers[key].apiKey) {
111
- configured++;
112
- }
265
+ async function loadAudioConfig() {
266
+ try {
267
+ const resp = await fetch('/api/audio-config');
268
+ audioConfigData = await resp.json();
269
+ renderProviders('audio');
270
+ if (currentTab === 'audio') updateCountBadge();
271
+ } catch (err) {
272
+ document.getElementById('audioProviderList').innerHTML =
273
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
113
274
  }
114
- document.getElementById('configCount').textContent = configured + '/' + total + ' 已配置';
275
+ }
276
+
277
+ // ==================== 渲染 ====================
278
+ function renderProviders(type) {
279
+ const data = type === 'llm' ? llmConfigData :
280
+ type === 'video' ? videoConfigData : audioConfigData;
281
+ const listEl = document.getElementById(
282
+ type === 'llm' ? 'providerList' :
283
+ type === 'video' ? 'videoProviderList' : 'audioProviderList'
284
+ );
285
+ if (!data) return;
286
+
287
+ const providers = data.providers;
288
+ const info = data.providerInfo;
289
+ const isActive = (key) => data.activeProvider === key;
115
290
 
116
- // 生成卡片
117
291
  let html = '';
118
292
  for (const key in providers) {
119
293
  const p = providers[key];
120
294
  const i = info[key] || { name: key, description: '供应商', requiresApiKey: true };
121
- const isActive = configData.activeProvider === key;
295
+ const active = isActive(key);
122
296
 
123
- // 状态判断
124
297
  let statusText, statusClass;
125
- if (isActive) {
298
+ if (active) {
126
299
  statusText = '使用中';
127
300
  statusClass = 'status-accent';
128
301
  } else if (p.enabled && p.apiKey) {
@@ -136,16 +309,16 @@
136
309
  statusClass = 'status-muted';
137
310
  }
138
311
 
139
- // 配置状态
140
312
  const isConfigured = p.enabled && p.apiKey;
141
313
 
142
314
  html += `
143
- <div class="provider-card ${isActive ? 'active' : ''}" onclick="openModal('${key}')">
315
+ <div class="provider-card ${active ? 'active' : ''}" onclick="openModal('${type}', '${key}')">
144
316
  <div class="provider-header">
145
317
  <div class="provider-icon">${i.name.charAt(0).toUpperCase()}</div>
146
318
  <div class="provider-info">
147
319
  <h3 class="provider-name">${i.name}</h3>
148
320
  <p class="provider-desc">${i.description}</p>
321
+ ${i.docs ? '<a class="provider-docs" href="' + i.docs + '" target="_blank" onclick="event.stopPropagation()">📖 文档</a>' : ''}
149
322
  </div>
150
323
  <div class="status-badge ${statusClass}">
151
324
  <span class="status-dot"></span>
@@ -163,35 +336,86 @@
163
336
  </div>
164
337
  `;
165
338
  }
166
- list.innerHTML = html;
339
+ listEl.innerHTML = html;
167
340
  }
168
341
 
169
- // 打开弹窗
170
- function openModal(providerKey) {
342
+ // ==================== 弹窗 ====================
343
+ function openModal(type, providerKey) {
344
+ currentProviderType = type;
171
345
  currentProvider = providerKey;
172
- const p = configData.providers[providerKey];
173
- const i = configData.providerInfo[providerKey] || { name: providerKey, description: '供应商' };
346
+ const data = type === 'llm' ? llmConfigData :
347
+ type === 'video' ? videoConfigData : audioConfigData;
348
+ const p = data.providers[providerKey];
349
+ const i = data.providerInfo[providerKey] || { name: providerKey, description: '供应商' };
174
350
 
175
351
  document.getElementById('modalIcon').textContent = i.name.charAt(0).toUpperCase();
176
352
  document.getElementById('modalTitle').textContent = '配置 ' + i.name;
177
353
  document.getElementById('modalSubtitle').textContent = i.description;
178
354
 
179
355
  document.getElementById('apiKeyInput').value = '';
356
+ document.getElementById('apiKeyHint').textContent = p.apiKey
357
+ ? '当前已配置 (***' + (p.apiKey.slice(-4) || '') + '),输入新值以更新'
358
+ : '输入 API Key';
180
359
  document.getElementById('baseUrlInput').value = p.baseUrl || '';
181
360
  document.getElementById('modelInput').value = p.model || '';
182
- document.getElementById('temperatureInput').value = p.temperature || 0.7;
361
+
362
+ // 填充模型下拉建议(仅 LLM 类型有 providerInfo.models)
363
+ const datalist = document.getElementById('modelSuggestList');
364
+ datalist.innerHTML = '';
365
+ if (type === 'llm' && i.models && Array.isArray(i.models)) {
366
+ for (const m of i.models) {
367
+ const opt = document.createElement('option');
368
+ opt.value = m;
369
+ datalist.appendChild(opt);
370
+ }
371
+ }
372
+
373
+ // 视频/音频/LLM 专用字段显隐
374
+ const isVideo = type === 'video';
375
+ const isAudio = type === 'audio';
376
+ document.querySelector('.video-only-fields').style.display = isVideo ? 'block' : 'none';
377
+ document.querySelector('.audio-only-fields').style.display = isAudio ? 'block' : 'none';
378
+ document.querySelector('.llm-only-fields').style.display = (!isVideo && !isAudio) ? 'block' : 'none';
379
+
380
+ if (isVideo) {
381
+ document.getElementById('resolutionInput').value = p.resolution || '720p';
382
+ document.getElementById('ratioInput').value = p.ratio || '16:9';
383
+ document.getElementById('durationInput').value = p.duration || 5;
384
+ document.getElementById('modelHint').textContent = '示例: doubao-seedance-1-0-lite-t2v-250428 (文生视频) 或 ...-i2v-... (图生视频)';
385
+ } else if (isAudio) {
386
+ const isMusic = (i.kind === 'music');
387
+ // TTS 字段
388
+ document.getElementById('voiceInput').parentElement.style.display = isMusic ? 'none' : 'block';
389
+ document.getElementById('speedInput').parentElement.parentElement.style.display = isMusic ? 'none' : 'flex';
390
+ document.getElementById('modeInput').parentElement.style.display = isMusic ? 'block' : 'none';
391
+ document.getElementById('audioDurationInput').parentElement.style.display = isMusic ? 'block' : 'none';
392
+
393
+ if (!isMusic) {
394
+ document.getElementById('voiceInput').value = p.voice || 'male-qn-jingying';
395
+ document.getElementById('voiceHint').textContent = '常用音色: male-qn-jingying, female-shaonv, female-yujie, presenter_male, presenter_female';
396
+ document.getElementById('speedInput').value = p.speed || 1.0;
397
+ document.getElementById('formatInput').value = p.format || 'mp3';
398
+ } else {
399
+ document.getElementById('modeInput').value = p.mode || 'instrumental';
400
+ document.getElementById('audioDurationInput').value = p.duration || 30;
401
+ }
402
+ document.getElementById('modelHint').textContent = isMusic
403
+ ? '示例: music-01'
404
+ : '示例: speech-01 (TTS) / asr-01 (语音转写)';
405
+ } else {
406
+ document.getElementById('temperatureInput').value = p.temperature || 0.7;
407
+ }
183
408
 
184
409
  document.getElementById('testResult').style.display = 'none';
185
410
  document.getElementById('configModal').style.display = 'flex';
186
411
  }
187
412
 
188
- // 关闭弹窗
189
413
  function closeModal() {
190
414
  document.getElementById('configModal').style.display = 'none';
191
415
  currentProvider = null;
192
416
  }
193
417
 
194
- // 测试连接
418
+ // ==================== 测试连接 ====================
195
419
  async function testConnection() {
196
420
  const btn = document.getElementById('testBtn');
197
421
  const result = document.getElementById('testResult');
@@ -200,8 +424,12 @@
200
424
  btn.innerHTML = '<span class="spinner"></span> 测试中...';
201
425
  result.style.display = 'none';
202
426
 
427
+ const endpoint = currentProviderType === 'llm' ? '/api/llm-test' :
428
+ currentProviderType === 'video' ? '/api/video-test' :
429
+ '/api/audio-test';
430
+
203
431
  try {
204
- const resp = await fetch('/api/llm-test', {
432
+ const resp = await fetch(endpoint, {
205
433
  method: 'POST',
206
434
  headers: { 'Content-Type': 'application/json' },
207
435
  body: JSON.stringify({ provider: currentProvider })
@@ -222,44 +450,71 @@
222
450
  btn.innerHTML = '⚡ 测试连接';
223
451
  }
224
452
 
225
- // 保存配置
453
+ // ==================== 保存 ====================
226
454
  document.getElementById('configForm').onsubmit = async function(e) {
227
455
  e.preventDefault();
228
456
 
457
+ const data = currentProviderType === 'llm' ? llmConfigData : videoConfigData;
458
+ const existing = data.providers[currentProvider];
459
+ const apiKeyVal = document.getElementById('apiKeyInput').value;
460
+
229
461
  const updateData = {
230
462
  enabled: true,
231
- apiKey: document.getElementById('apiKeyInput').value || configData.providers[currentProvider].apiKey || '',
232
- baseUrl: document.getElementById('baseUrlInput').value || configData.providers[currentProvider].baseUrl || '',
233
- model: document.getElementById('modelInput').value || configData.providers[currentProvider].model || '',
234
- temperature: parseFloat(document.getElementById('temperatureInput').value) || 0.7
463
+ apiKey: apiKeyVal || existing.apiKey || '',
464
+ baseUrl: document.getElementById('baseUrlInput').value || existing.baseUrl || '',
465
+ model: document.getElementById('modelInput').value || existing.model || ''
235
466
  };
236
467
 
468
+ if (currentProviderType === 'video') {
469
+ updateData.resolution = document.getElementById('resolutionInput').value;
470
+ updateData.ratio = document.getElementById('ratioInput').value;
471
+ updateData.duration = parseInt(document.getElementById('durationInput').value) || 5;
472
+ } else if (currentProviderType === 'audio') {
473
+ const info = audioConfigData.providerInfo[currentProvider] || {};
474
+ const isMusic = info.kind === 'music';
475
+ if (isMusic) {
476
+ updateData.mode = document.getElementById('modeInput').value;
477
+ updateData.duration = parseInt(document.getElementById('audioDurationInput').value) || 30;
478
+ } else {
479
+ updateData.voice = document.getElementById('voiceInput').value;
480
+ updateData.speed = parseFloat(document.getElementById('speedInput').value) || 1.0;
481
+ updateData.format = document.getElementById('formatInput').value;
482
+ }
483
+ } else {
484
+ updateData.temperature = parseFloat(document.getElementById('temperatureInput').value) || 0.7;
485
+ }
486
+
487
+ const endpoint = currentProviderType === 'llm' ? '/api/llm-config' :
488
+ currentProviderType === 'video' ? '/api/video-config' :
489
+ '/api/audio-config';
490
+
237
491
  try {
238
- await fetch('/api/llm-config', {
492
+ await fetch(endpoint, {
239
493
  method: 'POST',
240
494
  headers: { 'Content-Type': 'application/json' },
241
495
  body: JSON.stringify({ provider: currentProvider, config: updateData })
242
496
  });
243
497
 
244
- // 显示保存成功
245
498
  const saveBtn = this.querySelector('.btn-save');
246
499
  saveBtn.textContent = '✓ 已保存';
247
500
  setTimeout(() => {
248
501
  closeModal();
249
- loadConfig();
502
+ if (currentProviderType === 'llm') loadLLMConfig();
503
+ else if (currentProviderType === 'video') loadVideoConfig();
504
+ else loadAudioConfig();
250
505
  }, 500);
251
506
  } catch (err) {
252
507
  alert('保存失败: ' + err.message);
253
508
  }
254
509
  };
255
510
 
256
- // 点击遮罩关闭弹窗
511
+ // ==================== 关闭弹窗 ====================
257
512
  document.getElementById('configModal').onclick = function(e) {
258
513
  if (e.target === this) closeModal();
259
514
  };
260
515
 
261
516
  // 启动
262
- loadConfig();
517
+ loadAll();
263
518
  </script>
264
519
  </body>
265
- </html>
520
+ </html>