@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.
@@ -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,79 @@
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">
96
+ <p class="form-hint" id="modelHint"></p>
97
+ </div>
98
+
99
+ <!-- 视频模型专用字段 -->
100
+ <div class="video-only-fields" style="display: none;">
101
+ <div class="form-row">
102
+ <div class="form-group">
103
+ <label>分辨率</label>
104
+ <select id="resolutionInput">
105
+ <option value="480p">480p</option>
106
+ <option value="720p">720p</option>
107
+ <option value="1080p">1080p</option>
108
+ </select>
109
+ </div>
110
+ <div class="form-group">
111
+ <label>宽高比</label>
112
+ <select id="ratioInput">
113
+ <option value="16:9">16:9</option>
114
+ <option value="9:16">9:16</option>
115
+ <option value="1:1">1:1</option>
116
+ <option value="4:3">4:3</option>
117
+ <option value="3:4">3:4</option>
118
+ <option value="21:9">21:9</option>
119
+ </select>
120
+ </div>
121
+ </div>
122
+ <div class="form-group">
123
+ <label>默认时长(秒)</label>
124
+ <input type="number" id="durationInput" min="3" max="12" step="1" value="5">
125
+ <p class="form-hint">Seedance 支持 3-12 秒</p>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- 音频模型专用字段 -->
130
+ <div class="audio-only-fields" style="display: none;">
56
131
  <div class="form-group">
57
- <label>模型</label>
58
- <input type="text" id="modelInput" placeholder=" gpt-4">
132
+ <label>音色 / 风格</label>
133
+ <input type="text" id="voiceInput" placeholder="male-qn-jingying / female-shaonv ...">
134
+ <p class="form-hint" id="voiceHint"></p>
135
+ </div>
136
+ <div class="form-row">
137
+ <div class="form-group">
138
+ <label>语速</label>
139
+ <input type="number" id="speedInput" min="0.5" max="2.0" step="0.1" value="1.0">
140
+ </div>
141
+ <div class="form-group">
142
+ <label>输出格式</label>
143
+ <select id="formatInput">
144
+ <option value="mp3">mp3</option>
145
+ <option value="pcm">pcm</option>
146
+ <option value="wav">wav</option>
147
+ </select>
148
+ </div>
149
+ </div>
150
+ <div class="form-group" id="audioModeGroup" style="display: none;">
151
+ <label>模式</label>
152
+ <select id="modeInput">
153
+ <option value="instrumental">纯音乐</option>
154
+ <option value="lyrics">带歌词</option>
155
+ </select>
156
+ </div>
157
+ <div class="form-group" id="audioDurationGroup" style="display: none;">
158
+ <label>默认时长(秒)</label>
159
+ <input type="number" id="audioDurationInput" min="5" max="120" step="1" value="30">
160
+ <p class="form-hint">music-01 支持 5-120 秒</p>
59
161
  </div>
162
+ </div>
163
+
164
+ <!-- LLM 专用字段 -->
165
+ <div class="llm-only-fields">
60
166
  <div class="form-group form-group-small">
61
167
  <label>温度</label>
62
168
  <input type="number" id="temperatureInput" value="0.7" min="0" max="2" step="0.1">
@@ -80,49 +186,115 @@
80
186
  </div>
81
187
 
82
188
  <script>
83
- // 当前配置数据
84
- let configData = null;
189
+ // 当前 tab
190
+ let currentTab = 'llm';
191
+
192
+ // 缓存三份配置
193
+ let llmConfigData = null;
194
+ let videoConfigData = null;
195
+ let audioConfigData = null;
85
196
  let currentProvider = null;
197
+ let currentProviderType = 'llm'; // 'llm' | 'video' | 'audio'
198
+
199
+ // ==================== Tab 切换 ====================
200
+ function switchTab(tab) {
201
+ currentTab = tab;
202
+ document.querySelectorAll('.api-tab').forEach(btn => {
203
+ btn.classList.toggle('active', btn.dataset.tab === tab);
204
+ });
205
+ document.querySelectorAll('.api-panel').forEach(panel => {
206
+ panel.style.display = panel.dataset.panel === tab ? 'block' : 'none';
207
+ });
208
+ // 切换 tab 时更新计数 badge
209
+ updateCountBadge();
210
+ }
86
211
 
87
- // 加载配置
88
- async function loadConfig() {
212
+ function updateCountBadge() {
213
+ const data = currentTab === 'llm' ? llmConfigData :
214
+ currentTab === 'video' ? videoConfigData : audioConfigData;
215
+ if (!data) {
216
+ document.getElementById('configCount').textContent = '加载中...';
217
+ return;
218
+ }
219
+ let configured = 0, total = 0;
220
+ for (const key in data.providers) {
221
+ total++;
222
+ const p = data.providers[key];
223
+ if (currentTab === 'llm') {
224
+ if (p.enabled && p.apiKey) configured++;
225
+ } else {
226
+ const info = data.providerInfo[key] || {};
227
+ if (p.enabled && (!info.requiresApiKey || p.apiKey)) configured++;
228
+ }
229
+ }
230
+ const labelMap = { llm: '', video: '视频', audio: '音频' };
231
+ const label = labelMap[currentTab] || '';
232
+ document.getElementById('configCount').textContent = configured + '/' + total + ' ' + label + '已配置';
233
+ }
234
+
235
+ // ==================== 加载 ====================
236
+ async function loadAll() {
237
+ await Promise.all([loadLLMConfig(), loadVideoConfig(), loadAudioConfig()]);
238
+ }
239
+
240
+ async function loadLLMConfig() {
89
241
  try {
90
242
  const resp = await fetch('/api/llm-config');
91
- configData = await resp.json();
92
- renderProviders();
243
+ llmConfigData = await resp.json();
244
+ renderProviders('llm');
245
+ if (currentTab === 'llm') updateCountBadge();
93
246
  } catch (err) {
94
247
  document.getElementById('providerList').innerHTML =
95
248
  '<div class="error-state">加载失败: ' + err.message + '</div>';
96
249
  }
97
250
  }
98
251
 
99
- // 渲染提供商列表
100
- function renderProviders() {
101
- const list = document.getElementById('providerList');
102
- const providers = configData.providers;
103
- const info = configData.providerInfo;
252
+ async function loadVideoConfig() {
253
+ try {
254
+ const resp = await fetch('/api/video-config');
255
+ videoConfigData = await resp.json();
256
+ renderProviders('video');
257
+ if (currentTab === 'video') updateCountBadge();
258
+ } catch (err) {
259
+ document.getElementById('videoProviderList').innerHTML =
260
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
261
+ }
262
+ }
104
263
 
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
- }
264
+ async function loadAudioConfig() {
265
+ try {
266
+ const resp = await fetch('/api/audio-config');
267
+ audioConfigData = await resp.json();
268
+ renderProviders('audio');
269
+ if (currentTab === 'audio') updateCountBadge();
270
+ } catch (err) {
271
+ document.getElementById('audioProviderList').innerHTML =
272
+ '<div class="error-state">加载失败: ' + err.message + '</div>';
113
273
  }
114
- document.getElementById('configCount').textContent = configured + '/' + total + ' 已配置';
274
+ }
275
+
276
+ // ==================== 渲染 ====================
277
+ function renderProviders(type) {
278
+ const data = type === 'llm' ? llmConfigData :
279
+ type === 'video' ? videoConfigData : audioConfigData;
280
+ const listEl = document.getElementById(
281
+ type === 'llm' ? 'providerList' :
282
+ type === 'video' ? 'videoProviderList' : 'audioProviderList'
283
+ );
284
+ if (!data) return;
285
+
286
+ const providers = data.providers;
287
+ const info = data.providerInfo;
288
+ const isActive = (key) => data.activeProvider === key;
115
289
 
116
- // 生成卡片
117
290
  let html = '';
118
291
  for (const key in providers) {
119
292
  const p = providers[key];
120
293
  const i = info[key] || { name: key, description: '供应商', requiresApiKey: true };
121
- const isActive = configData.activeProvider === key;
294
+ const active = isActive(key);
122
295
 
123
- // 状态判断
124
296
  let statusText, statusClass;
125
- if (isActive) {
297
+ if (active) {
126
298
  statusText = '使用中';
127
299
  statusClass = 'status-accent';
128
300
  } else if (p.enabled && p.apiKey) {
@@ -136,16 +308,16 @@
136
308
  statusClass = 'status-muted';
137
309
  }
138
310
 
139
- // 配置状态
140
311
  const isConfigured = p.enabled && p.apiKey;
141
312
 
142
313
  html += `
143
- <div class="provider-card ${isActive ? 'active' : ''}" onclick="openModal('${key}')">
314
+ <div class="provider-card ${active ? 'active' : ''}" onclick="openModal('${type}', '${key}')">
144
315
  <div class="provider-header">
145
316
  <div class="provider-icon">${i.name.charAt(0).toUpperCase()}</div>
146
317
  <div class="provider-info">
147
318
  <h3 class="provider-name">${i.name}</h3>
148
319
  <p class="provider-desc">${i.description}</p>
320
+ ${i.docs ? '<a class="provider-docs" href="' + i.docs + '" target="_blank" onclick="event.stopPropagation()">📖 文档</a>' : ''}
149
321
  </div>
150
322
  <div class="status-badge ${statusClass}">
151
323
  <span class="status-dot"></span>
@@ -163,35 +335,75 @@
163
335
  </div>
164
336
  `;
165
337
  }
166
- list.innerHTML = html;
338
+ listEl.innerHTML = html;
167
339
  }
168
340
 
169
- // 打开弹窗
170
- function openModal(providerKey) {
341
+ // ==================== 弹窗 ====================
342
+ function openModal(type, providerKey) {
343
+ currentProviderType = type;
171
344
  currentProvider = providerKey;
172
- const p = configData.providers[providerKey];
173
- const i = configData.providerInfo[providerKey] || { name: providerKey, description: '供应商' };
345
+ const data = type === 'llm' ? llmConfigData :
346
+ type === 'video' ? videoConfigData : audioConfigData;
347
+ const p = data.providers[providerKey];
348
+ const i = data.providerInfo[providerKey] || { name: providerKey, description: '供应商' };
174
349
 
175
350
  document.getElementById('modalIcon').textContent = i.name.charAt(0).toUpperCase();
176
351
  document.getElementById('modalTitle').textContent = '配置 ' + i.name;
177
352
  document.getElementById('modalSubtitle').textContent = i.description;
178
353
 
179
354
  document.getElementById('apiKeyInput').value = '';
355
+ document.getElementById('apiKeyHint').textContent = p.apiKey
356
+ ? '当前已配置 (***' + (p.apiKey.slice(-4) || '') + '),输入新值以更新'
357
+ : '输入 API Key';
180
358
  document.getElementById('baseUrlInput').value = p.baseUrl || '';
181
359
  document.getElementById('modelInput').value = p.model || '';
182
- document.getElementById('temperatureInput').value = p.temperature || 0.7;
360
+
361
+ // 视频/音频/LLM 专用字段显隐
362
+ const isVideo = type === 'video';
363
+ const isAudio = type === 'audio';
364
+ document.querySelector('.video-only-fields').style.display = isVideo ? 'block' : 'none';
365
+ document.querySelector('.audio-only-fields').style.display = isAudio ? 'block' : 'none';
366
+ document.querySelector('.llm-only-fields').style.display = (!isVideo && !isAudio) ? 'block' : 'none';
367
+
368
+ if (isVideo) {
369
+ document.getElementById('resolutionInput').value = p.resolution || '720p';
370
+ document.getElementById('ratioInput').value = p.ratio || '16:9';
371
+ document.getElementById('durationInput').value = p.duration || 5;
372
+ document.getElementById('modelHint').textContent = '示例: doubao-seedance-1-0-lite-t2v-250428 (文生视频) 或 ...-i2v-... (图生视频)';
373
+ } else if (isAudio) {
374
+ const isMusic = (i.kind === 'music');
375
+ // TTS 字段
376
+ document.getElementById('voiceInput').parentElement.style.display = isMusic ? 'none' : 'block';
377
+ document.getElementById('speedInput').parentElement.parentElement.style.display = isMusic ? 'none' : 'flex';
378
+ document.getElementById('modeInput').parentElement.style.display = isMusic ? 'block' : 'none';
379
+ document.getElementById('audioDurationInput').parentElement.style.display = isMusic ? 'block' : 'none';
380
+
381
+ if (!isMusic) {
382
+ document.getElementById('voiceInput').value = p.voice || 'male-qn-jingying';
383
+ document.getElementById('voiceHint').textContent = '常用音色: male-qn-jingying, female-shaonv, female-yujie, presenter_male, presenter_female';
384
+ document.getElementById('speedInput').value = p.speed || 1.0;
385
+ document.getElementById('formatInput').value = p.format || 'mp3';
386
+ } else {
387
+ document.getElementById('modeInput').value = p.mode || 'instrumental';
388
+ document.getElementById('audioDurationInput').value = p.duration || 30;
389
+ }
390
+ document.getElementById('modelHint').textContent = isMusic
391
+ ? '示例: music-01'
392
+ : '示例: speech-01 (TTS) / asr-01 (语音转写)';
393
+ } else {
394
+ document.getElementById('temperatureInput').value = p.temperature || 0.7;
395
+ }
183
396
 
184
397
  document.getElementById('testResult').style.display = 'none';
185
398
  document.getElementById('configModal').style.display = 'flex';
186
399
  }
187
400
 
188
- // 关闭弹窗
189
401
  function closeModal() {
190
402
  document.getElementById('configModal').style.display = 'none';
191
403
  currentProvider = null;
192
404
  }
193
405
 
194
- // 测试连接
406
+ // ==================== 测试连接 ====================
195
407
  async function testConnection() {
196
408
  const btn = document.getElementById('testBtn');
197
409
  const result = document.getElementById('testResult');
@@ -200,8 +412,12 @@
200
412
  btn.innerHTML = '<span class="spinner"></span> 测试中...';
201
413
  result.style.display = 'none';
202
414
 
415
+ const endpoint = currentProviderType === 'llm' ? '/api/llm-test' :
416
+ currentProviderType === 'video' ? '/api/video-test' :
417
+ '/api/audio-test';
418
+
203
419
  try {
204
- const resp = await fetch('/api/llm-test', {
420
+ const resp = await fetch(endpoint, {
205
421
  method: 'POST',
206
422
  headers: { 'Content-Type': 'application/json' },
207
423
  body: JSON.stringify({ provider: currentProvider })
@@ -222,44 +438,71 @@
222
438
  btn.innerHTML = '⚡ 测试连接';
223
439
  }
224
440
 
225
- // 保存配置
441
+ // ==================== 保存 ====================
226
442
  document.getElementById('configForm').onsubmit = async function(e) {
227
443
  e.preventDefault();
228
444
 
445
+ const data = currentProviderType === 'llm' ? llmConfigData : videoConfigData;
446
+ const existing = data.providers[currentProvider];
447
+ const apiKeyVal = document.getElementById('apiKeyInput').value;
448
+
229
449
  const updateData = {
230
450
  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
451
+ apiKey: apiKeyVal || existing.apiKey || '',
452
+ baseUrl: document.getElementById('baseUrlInput').value || existing.baseUrl || '',
453
+ model: document.getElementById('modelInput').value || existing.model || ''
235
454
  };
236
455
 
456
+ if (currentProviderType === 'video') {
457
+ updateData.resolution = document.getElementById('resolutionInput').value;
458
+ updateData.ratio = document.getElementById('ratioInput').value;
459
+ updateData.duration = parseInt(document.getElementById('durationInput').value) || 5;
460
+ } else if (currentProviderType === 'audio') {
461
+ const info = audioConfigData.providerInfo[currentProvider] || {};
462
+ const isMusic = info.kind === 'music';
463
+ if (isMusic) {
464
+ updateData.mode = document.getElementById('modeInput').value;
465
+ updateData.duration = parseInt(document.getElementById('audioDurationInput').value) || 30;
466
+ } else {
467
+ updateData.voice = document.getElementById('voiceInput').value;
468
+ updateData.speed = parseFloat(document.getElementById('speedInput').value) || 1.0;
469
+ updateData.format = document.getElementById('formatInput').value;
470
+ }
471
+ } else {
472
+ updateData.temperature = parseFloat(document.getElementById('temperatureInput').value) || 0.7;
473
+ }
474
+
475
+ const endpoint = currentProviderType === 'llm' ? '/api/llm-config' :
476
+ currentProviderType === 'video' ? '/api/video-config' :
477
+ '/api/audio-config';
478
+
237
479
  try {
238
- await fetch('/api/llm-config', {
480
+ await fetch(endpoint, {
239
481
  method: 'POST',
240
482
  headers: { 'Content-Type': 'application/json' },
241
483
  body: JSON.stringify({ provider: currentProvider, config: updateData })
242
484
  });
243
485
 
244
- // 显示保存成功
245
486
  const saveBtn = this.querySelector('.btn-save');
246
487
  saveBtn.textContent = '✓ 已保存';
247
488
  setTimeout(() => {
248
489
  closeModal();
249
- loadConfig();
490
+ if (currentProviderType === 'llm') loadLLMConfig();
491
+ else if (currentProviderType === 'video') loadVideoConfig();
492
+ else loadAudioConfig();
250
493
  }, 500);
251
494
  } catch (err) {
252
495
  alert('保存失败: ' + err.message);
253
496
  }
254
497
  };
255
498
 
256
- // 点击遮罩关闭弹窗
499
+ // ==================== 关闭弹窗 ====================
257
500
  document.getElementById('configModal').onclick = function(e) {
258
501
  if (e.target === this) closeModal();
259
502
  };
260
503
 
261
504
  // 启动
262
- loadConfig();
505
+ loadAll();
263
506
  </script>
264
507
  </body>
265
- </html>
508
+ </html>
@@ -10,6 +10,7 @@ import { documentReader } from '../documents/reader.js';
10
10
  import { initMinimax, getMinimax } from '../constraints/index.js';
11
11
  import { createAgentSession } from '../agents/pi-sdk.js';
12
12
  import { llmConfigStore } from '../llm/config-store.js';
13
+ import { videoConfigStore } from '../llm/video-config-store.js';
13
14
  import { irohTransport } from '../network/iroh-transport.js';
14
15
  import { createAgentDelegateApp } from './agent-delegate-server.js';
15
16
  import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
@@ -2010,6 +2011,60 @@ export async function createWebServer(port = 3000, options = {}) {
2010
2011
  res.status(500).json({ error: err.message });
2011
2012
  }
2012
2013
  });
2014
+ // ==================== 视频生成配置 (Seedance 等) ====================
2015
+ // 获取视频生成配置
2016
+ app.get('/api/video-config', async (req, res) => {
2017
+ try {
2018
+ const config = await videoConfigStore.getConfig();
2019
+ const providerInfo = videoConfigStore.getAllProviderInfo();
2020
+ // 脱敏:不返回 apiKey 明文
2021
+ const masked = Object.fromEntries(Object.entries(config.providers).map(([key, val]) => [
2022
+ key,
2023
+ { ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
2024
+ ]));
2025
+ res.json({
2026
+ activeProvider: config.activeProvider,
2027
+ providers: masked,
2028
+ providerInfo
2029
+ });
2030
+ }
2031
+ catch (err) {
2032
+ res.status(500).json({ error: err.message });
2033
+ }
2034
+ });
2035
+ // 更新视频供应商配置
2036
+ app.post('/api/video-config', async (req, res) => {
2037
+ try {
2038
+ const { provider, config } = req.body;
2039
+ if (!provider || !config) {
2040
+ return res.status(400).json({ error: 'provider and config required' });
2041
+ }
2042
+ // 如果前端发的是掩码(***xxx),从当前配置里取真实 key
2043
+ const currentConfig = await videoConfigStore.getProvider(provider);
2044
+ if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
2045
+ config.apiKey = currentConfig.apiKey;
2046
+ }
2047
+ await videoConfigStore.updateProvider(provider, config);
2048
+ res.json({ ok: true });
2049
+ }
2050
+ catch (err) {
2051
+ res.status(500).json({ error: err.message });
2052
+ }
2053
+ });
2054
+ // 测试视频供应商连接
2055
+ app.post('/api/video-test', async (req, res) => {
2056
+ try {
2057
+ const { provider } = req.body;
2058
+ if (!provider) {
2059
+ return res.status(400).json({ error: 'provider required' });
2060
+ }
2061
+ const result = await videoConfigStore.testProvider(provider);
2062
+ res.json(result);
2063
+ }
2064
+ catch (err) {
2065
+ res.status(500).json({ error: err.message });
2066
+ }
2067
+ });
2013
2068
  // 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
2014
2069
  // 入参: { text, mimeType, fileName, fromNodeId, source }
2015
2070
  // 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
@@ -2989,6 +2989,89 @@ body {
2989
2989
  max-width: 900px;
2990
2990
  margin: 0 auto;
2991
2991
  padding: 24px;
2992
+ min-height: 100vh;
2993
+ }
2994
+
2995
+ /* Standalone api-config page: enable page-level scrolling.
2996
+ Default body has overflow:hidden (for app shell with sidebar). */
2997
+ body:has(> .api-config-page) {
2998
+ height: auto;
2999
+ min-height: 100vh;
3000
+ overflow-y: auto;
3001
+ }
3002
+
3003
+ /* Tab switcher */
3004
+ .api-tabs {
3005
+ display: flex;
3006
+ gap: 4px;
3007
+ border-bottom: 1px solid var(--border);
3008
+ margin-bottom: 24px;
3009
+ }
3010
+
3011
+ .api-tab {
3012
+ display: flex;
3013
+ align-items: center;
3014
+ gap: 8px;
3015
+ padding: 12px 20px;
3016
+ background: transparent;
3017
+ border: none;
3018
+ border-bottom: 2px solid transparent;
3019
+ color: var(--text-muted);
3020
+ font-size: 14px;
3021
+ font-weight: 500;
3022
+ cursor: pointer;
3023
+ transition: all 0.2s;
3024
+ }
3025
+
3026
+ .api-tab:hover {
3027
+ color: var(--text);
3028
+ }
3029
+
3030
+ .api-tab.active {
3031
+ color: var(--accent);
3032
+ border-bottom-color: var(--accent);
3033
+ }
3034
+
3035
+ .api-tab-icon {
3036
+ font-size: 16px;
3037
+ }
3038
+
3039
+ .api-panel {
3040
+ animation: fadeIn 0.2s ease;
3041
+ }
3042
+
3043
+ @keyframes fadeIn {
3044
+ from { opacity: 0; transform: translateY(4px); }
3045
+ to { opacity: 1; transform: translateY(0); }
3046
+ }
3047
+
3048
+ .video-intro {
3049
+ background: var(--bg-sidebar);
3050
+ border: 1px solid var(--border);
3051
+ border-radius: var(--radius);
3052
+ padding: 12px 16px;
3053
+ margin-bottom: 16px;
3054
+ color: var(--text-muted);
3055
+ font-size: 13px;
3056
+ line-height: 1.6;
3057
+ }
3058
+
3059
+ .video-intro p {
3060
+ margin: 0;
3061
+ }
3062
+
3063
+ .provider-docs {
3064
+ display: inline-block;
3065
+ margin-top: 4px;
3066
+ font-size: 12px;
3067
+ color: var(--accent);
3068
+ text-decoration: none;
3069
+ opacity: 0.8;
3070
+ }
3071
+
3072
+ .provider-docs:hover {
3073
+ opacity: 1;
3074
+ text-decoration: underline;
2992
3075
  }
2993
3076
 
2994
3077
  .loading-state {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",