@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/web/api-config.html
CHANGED
|
@@ -20,9 +20,47 @@
|
|
|
20
20
|
<a href="/" class="back-link">← 返回主页</a>
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
|
-
<!--
|
|
24
|
-
<div class="
|
|
25
|
-
<
|
|
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-
|
|
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
|
|
58
|
-
<input type="text" id="
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
294
|
+
const active = isActive(key);
|
|
122
295
|
|
|
123
|
-
// 状态判断
|
|
124
296
|
let statusText, statusClass;
|
|
125
|
-
if (
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
173
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
232
|
-
baseUrl: document.getElementById('baseUrlInput').value ||
|
|
233
|
-
model: document.getElementById('modelInput').value ||
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
505
|
+
loadAll();
|
|
263
506
|
</script>
|
|
264
507
|
</body>
|
|
265
|
-
</html>
|
|
508
|
+
</html>
|
package/src/web/server.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { documentReader } from '../documents/reader.js';
|
|
|
18
18
|
import { initMinimax, getMinimax } from '../constraints/index.js';
|
|
19
19
|
import { createAgentSession, type AgentSession, type StreamCallback, type StreamEvent } from '../agents/pi-sdk.js';
|
|
20
20
|
import { llmConfigStore, type ModelProvider, PROVIDER_INFO } from '../llm/config-store.js';
|
|
21
|
+
import { videoConfigStore, type VideoProvider } from '../llm/video-config-store.js';
|
|
22
|
+
import { audioConfigStore, type AudioProvider } from '../llm/audio-config-store.js';
|
|
21
23
|
import { irohTransport } from '../network/iroh-transport.js';
|
|
22
24
|
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
23
25
|
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
@@ -2276,6 +2278,130 @@ app.get('/channels', async (_req, res) => {
|
|
|
2276
2278
|
}
|
|
2277
2279
|
});
|
|
2278
2280
|
|
|
2281
|
+
// ==================== 视频生成配置 (Seedance 等) ====================
|
|
2282
|
+
|
|
2283
|
+
// 获取视频生成配置
|
|
2284
|
+
app.get('/api/video-config', async (req, res) => {
|
|
2285
|
+
try {
|
|
2286
|
+
const config = await videoConfigStore.getConfig();
|
|
2287
|
+
const providerInfo = videoConfigStore.getAllProviderInfo();
|
|
2288
|
+
|
|
2289
|
+
// 脱敏:不返回 apiKey 明文
|
|
2290
|
+
const masked = Object.fromEntries(
|
|
2291
|
+
Object.entries(config.providers).map(([key, val]) => [
|
|
2292
|
+
key,
|
|
2293
|
+
{ ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
|
|
2294
|
+
])
|
|
2295
|
+
);
|
|
2296
|
+
|
|
2297
|
+
res.json({
|
|
2298
|
+
activeProvider: config.activeProvider,
|
|
2299
|
+
providers: masked,
|
|
2300
|
+
providerInfo
|
|
2301
|
+
});
|
|
2302
|
+
} catch (err: any) {
|
|
2303
|
+
res.status(500).json({ error: err.message });
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
// 更新视频供应商配置
|
|
2308
|
+
app.post('/api/video-config', async (req, res) => {
|
|
2309
|
+
try {
|
|
2310
|
+
const { provider, config } = req.body;
|
|
2311
|
+
|
|
2312
|
+
if (!provider || !config) {
|
|
2313
|
+
return res.status(400).json({ error: 'provider and config required' });
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// 如果前端发的是掩码(***xxx),从当前配置里取真实 key
|
|
2317
|
+
const currentConfig = await videoConfigStore.getProvider(provider as VideoProvider);
|
|
2318
|
+
if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
|
|
2319
|
+
config.apiKey = currentConfig.apiKey;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
await videoConfigStore.updateProvider(provider as VideoProvider, config);
|
|
2323
|
+
res.json({ ok: true });
|
|
2324
|
+
} catch (err: any) {
|
|
2325
|
+
res.status(500).json({ error: err.message });
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
// 测试视频供应商连接
|
|
2330
|
+
app.post('/api/video-test', async (req, res) => {
|
|
2331
|
+
try {
|
|
2332
|
+
const { provider } = req.body;
|
|
2333
|
+
|
|
2334
|
+
if (!provider) {
|
|
2335
|
+
return res.status(400).json({ error: 'provider required' });
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const result = await videoConfigStore.testProvider(provider as VideoProvider);
|
|
2339
|
+
res.json(result);
|
|
2340
|
+
} catch (err: any) {
|
|
2341
|
+
res.status(500).json({ error: err.message });
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
// ==================== 音频生成配置 (TTS / Music) ====================
|
|
2346
|
+
|
|
2347
|
+
// 获取音频配置
|
|
2348
|
+
app.get('/api/audio-config', async (req, res) => {
|
|
2349
|
+
try {
|
|
2350
|
+
const config = await audioConfigStore.getConfig();
|
|
2351
|
+
const providerInfo = audioConfigStore.getAllProviderInfo();
|
|
2352
|
+
|
|
2353
|
+
const masked = Object.fromEntries(
|
|
2354
|
+
Object.entries(config.providers).map(([key, val]) => [
|
|
2355
|
+
key,
|
|
2356
|
+
{ ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
|
|
2357
|
+
])
|
|
2358
|
+
);
|
|
2359
|
+
|
|
2360
|
+
res.json({
|
|
2361
|
+
activeProvider: config.activeProvider,
|
|
2362
|
+
providers: masked,
|
|
2363
|
+
providerInfo
|
|
2364
|
+
});
|
|
2365
|
+
} catch (err: any) {
|
|
2366
|
+
res.status(500).json({ error: err.message });
|
|
2367
|
+
}
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
// 更新音频供应商配置
|
|
2371
|
+
app.post('/api/audio-config', async (req, res) => {
|
|
2372
|
+
try {
|
|
2373
|
+
const { provider, config } = req.body;
|
|
2374
|
+
if (!provider || !config) {
|
|
2375
|
+
return res.status(400).json({ error: 'provider and config required' });
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// 掩码回写真实 key
|
|
2379
|
+
const currentConfig = await audioConfigStore.getProvider(provider as AudioProvider);
|
|
2380
|
+
if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
|
|
2381
|
+
config.apiKey = currentConfig.apiKey;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
await audioConfigStore.updateProvider(provider as AudioProvider, config);
|
|
2385
|
+
res.json({ ok: true });
|
|
2386
|
+
} catch (err: any) {
|
|
2387
|
+
res.status(500).json({ error: err.message });
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
// 测试音频供应商连接
|
|
2392
|
+
app.post('/api/audio-test', async (req, res) => {
|
|
2393
|
+
try {
|
|
2394
|
+
const { provider } = req.body;
|
|
2395
|
+
if (!provider) {
|
|
2396
|
+
return res.status(400).json({ error: 'provider required' });
|
|
2397
|
+
}
|
|
2398
|
+
const result = await audioConfigStore.testProvider(provider as AudioProvider);
|
|
2399
|
+
res.json(result);
|
|
2400
|
+
} catch (err: any) {
|
|
2401
|
+
res.status(500).json({ error: err.message });
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2279
2405
|
// 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
|
|
2280
2406
|
// 入参: { text, mimeType, fileName, fromNodeId, source }
|
|
2281
2407
|
// 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
|