@bolloon/bolloon-agent 0.1.29 → 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/client.js +43 -15
- package/dist/web/server.js +81 -4
- 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/config-store.ts +61 -1
- package/src/llm/pi-ai.ts +25 -1
- package/src/llm/video-config-store.ts +251 -0
- package/src/web/api-config.html +296 -53
- package/src/web/client.js +43 -15
- package/src/web/server.ts +151 -4
- package/src/web/style.css +83 -0
package/dist/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/dist/web/client.js
CHANGED
|
@@ -2862,43 +2862,71 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2862
2862
|
loadHistory();
|
|
2863
2863
|
}
|
|
2864
2864
|
|
|
2865
|
-
// Phase 3: 我的 ID 按钮 →
|
|
2865
|
+
// Phase 3: 我的 ID 按钮 → 真 modal (避免 confirm 在某些环境被禁用)
|
|
2866
2866
|
const showMyIdBtn = document.getElementById('show-my-p2p-id-btn');
|
|
2867
2867
|
if (showMyIdBtn) {
|
|
2868
2868
|
showMyIdBtn.addEventListener('click', async (e) => {
|
|
2869
2869
|
e.stopPropagation();
|
|
2870
|
+
// 移除已有 modal
|
|
2871
|
+
document.getElementById('my-p2p-id-modal')?.remove();
|
|
2872
|
+
// 立即弹出 loading 状态 modal
|
|
2873
|
+
const html = `
|
|
2874
|
+
<div id="my-p2p-id-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10003;display:flex;align-items:center;justify-content:center;">
|
|
2875
|
+
<div style="background:#fff;border-radius:8px;width:480px;max-width:92vw;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,0.2);">
|
|
2876
|
+
<div style="padding:14px 18px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between;">
|
|
2877
|
+
<div style="font-size:15px;font-weight:600;">🪪 我的 P2P 身份</div>
|
|
2878
|
+
<button id="mpim-close" style="background:none;border:none;font-size:20px;color:#6b7280;cursor:pointer;">×</button>
|
|
2879
|
+
</div>
|
|
2880
|
+
<div id="mpim-body" style="padding:16px 18px;">
|
|
2881
|
+
<div style="color:#6b7280;font-size:13px;margin-bottom:10px;">正在获取 publicKey…</div>
|
|
2882
|
+
</div>
|
|
2883
|
+
</div>
|
|
2884
|
+
</div>
|
|
2885
|
+
`;
|
|
2886
|
+
document.body.insertAdjacentHTML('beforeend', html);
|
|
2887
|
+
document.getElementById('mpim-close').onclick = () => document.getElementById('my-p2p-id-modal').remove();
|
|
2888
|
+
|
|
2870
2889
|
try {
|
|
2871
2890
|
const res = await fetch('/api/p2p-publickey');
|
|
2872
2891
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2873
2892
|
const data = await res.json();
|
|
2874
2893
|
const pk = data.publicKey || '';
|
|
2894
|
+
const body = document.getElementById('mpim-body');
|
|
2875
2895
|
if (!pk || pk.length !== 64) {
|
|
2876
|
-
|
|
2896
|
+
body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ P2PDirect 还没启动, 刷新页面稍后再试</div>`;
|
|
2877
2897
|
return;
|
|
2878
2898
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2899
|
+
body.innerHTML = `
|
|
2900
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:8px;">把下面这串发给好友, 好友在 P2P 好友区点 "+ 好友" 粘贴即可加你:</div>
|
|
2901
|
+
<div style="display:flex;gap:6px;align-items:center;margin-bottom:12px;">
|
|
2902
|
+
<code id="mpim-pk" style="flex:1;padding:8px 10px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:4px;font-family:monospace;font-size:11px;word-break:break-all;line-height:1.4;">${escapeHtml(pk)}</code>
|
|
2903
|
+
<button id="mpim-copy" style="padding:8px 14px;background:#2563eb;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;white-space:nowrap;">📋 复制</button>
|
|
2904
|
+
</div>
|
|
2905
|
+
<div id="mpim-status" style="font-size:12px;color:#059669;min-height:16px;"></div>
|
|
2906
|
+
<div style="margin-top:14px;padding-top:12px;border-top:1px solid #e5e7eb;font-size:11px;color:#6b7280;">
|
|
2907
|
+
💡 同一个 role 重启后 publicKey 不会变, 好友不需要重新加你.
|
|
2908
|
+
</div>
|
|
2909
|
+
`;
|
|
2910
|
+
document.getElementById('mpim-copy').onclick = async () => {
|
|
2911
|
+
const statusEl = document.getElementById('mpim-status');
|
|
2886
2912
|
try {
|
|
2887
2913
|
await navigator.clipboard.writeText(pk);
|
|
2888
|
-
|
|
2914
|
+
statusEl.textContent = '✓ 已复制到剪贴板';
|
|
2889
2915
|
} catch {
|
|
2890
|
-
// 旧浏览器 fallback
|
|
2891
2916
|
const ta = document.createElement('textarea');
|
|
2892
2917
|
ta.value = pk;
|
|
2918
|
+
ta.style.position = 'fixed';
|
|
2919
|
+
ta.style.opacity = '0';
|
|
2893
2920
|
document.body.appendChild(ta);
|
|
2894
2921
|
ta.select();
|
|
2895
|
-
document.execCommand('copy');
|
|
2922
|
+
try { document.execCommand('copy'); statusEl.textContent = '✓ 已复制 (fallback)'; }
|
|
2923
|
+
catch { statusEl.textContent = '✗ 复制失败, 请手动选中复制'; }
|
|
2896
2924
|
document.body.removeChild(ta);
|
|
2897
|
-
alert('✓ 已复制到剪贴板 (fallback)');
|
|
2898
2925
|
}
|
|
2899
|
-
}
|
|
2926
|
+
};
|
|
2900
2927
|
} catch (err) {
|
|
2901
|
-
|
|
2928
|
+
const body = document.getElementById('mpim-body');
|
|
2929
|
+
if (body) body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ 获取失败: ${escapeHtml(err.message || String(err))}</div>`;
|
|
2902
2930
|
}
|
|
2903
2931
|
});
|
|
2904
2932
|
}
|