ibi-ai-talk 1.0.0 → 1.0.2
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.common.js +923 -922
- package/dist/index.common.js.map +1 -1
- package/dist/index.umd.js +923 -922
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/package.json +2 -2
- package/src/index.vue +1 -0
package/dist/index.umd.js
CHANGED
|
@@ -61,7 +61,7 @@ if (typeof window !== 'undefined') {
|
|
|
61
61
|
// Indicate to webpack that this file can be concatenated
|
|
62
62
|
/* harmony default export */ const setPublicPath = (null);
|
|
63
63
|
|
|
64
|
-
;// CONCATENATED MODULE: ./node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[2]!./node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/index.vue?vue&type=template&id=
|
|
64
|
+
;// CONCATENATED MODULE: ./node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[2]!./node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/index.vue?vue&type=template&id=70fe37c2
|
|
65
65
|
var render = function render(){var _vm=this,_c=_vm._self._c;return _c("div")
|
|
66
66
|
}
|
|
67
67
|
var staticRenderFns = []
|
|
@@ -237,1074 +237,1074 @@ function initOpusEncoder() {
|
|
|
237
237
|
return false;
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
-
;// CONCATENATED MODULE: ./src/utils/
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let mcpProperties = [];
|
|
245
|
-
let websocket = null; // 将从外部设置
|
|
240
|
+
;// CONCATENATED MODULE: ./src/utils/blocking-queue.js
|
|
241
|
+
class BlockingQueue {
|
|
242
|
+
#items = [];
|
|
243
|
+
#waiters = []; // {resolve, reject, min, timer, onTimeout}
|
|
246
244
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
*/
|
|
251
|
-
function setWebSocket(ws) {
|
|
252
|
-
websocket = ws;
|
|
253
|
-
}
|
|
245
|
+
/* 空队列一次性闸门 */
|
|
246
|
+
#emptyPromise = null;
|
|
247
|
+
#emptyResolve = null;
|
|
254
248
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
/* 生产者:把数据塞进去 */
|
|
250
|
+
enqueue(item, ...restItems) {
|
|
251
|
+
if (restItems.length === 0) {
|
|
252
|
+
this.#items.push(item);
|
|
253
|
+
}
|
|
254
|
+
// 如果有额外参数,批量处理所有项
|
|
255
|
+
else {
|
|
256
|
+
const items = [item, ...restItems].filter(i => i);
|
|
257
|
+
if (items.length === 0) return;
|
|
258
|
+
this.#items.push(...items);
|
|
259
|
+
}
|
|
260
|
+
// 若有空队列闸门,一次性放行所有等待者
|
|
261
|
+
if (this.#emptyResolve) {
|
|
262
|
+
this.#emptyResolve();
|
|
263
|
+
this.#emptyResolve = null;
|
|
264
|
+
this.#emptyPromise = null;
|
|
265
|
+
}
|
|
263
266
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
mcpTools = JSON.parse(savedTools);
|
|
268
|
-
} catch (e) {
|
|
269
|
-
console.log("加载MCP工具失败,使用默认工具", "warning");
|
|
270
|
-
mcpTools = [...defaultMcpTools];
|
|
267
|
+
// 唤醒所有正在等的 waiter
|
|
268
|
+
this.#wakeWaiters();
|
|
271
269
|
}
|
|
272
|
-
} else {
|
|
273
|
-
mcpTools = [...defaultMcpTools];
|
|
274
|
-
}
|
|
275
270
|
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
/* 消费者:min 条或 timeout ms 先到谁 */
|
|
272
|
+
async dequeue(min = 1, timeout = Infinity, onTimeout = null) {
|
|
273
|
+
// 1. 若空,等第一次数据到达(所有调用共享同一个 promise)
|
|
274
|
+
if (this.#items.length === 0) {
|
|
275
|
+
await this.#waitForFirstItem();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 立即满足
|
|
279
|
+
if (this.#items.length >= min) {
|
|
280
|
+
return this.#flush();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 需要等待
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
let timer = null;
|
|
286
|
+
const waiter = { resolve, reject, min, onTimeout, timer };
|
|
287
|
+
|
|
288
|
+
// 超时逻辑
|
|
289
|
+
if (Number.isFinite(timeout)) {
|
|
290
|
+
waiter.timer = setTimeout(() => {
|
|
291
|
+
this.#removeWaiter(waiter);
|
|
292
|
+
if (onTimeout) onTimeout(this.#items.length);
|
|
293
|
+
resolve(this.#flush());
|
|
294
|
+
}, timeout);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.#waiters.push(waiter);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* 空队列闸门生成器 */
|
|
302
|
+
#waitForFirstItem() {
|
|
303
|
+
if (!this.#emptyPromise) {
|
|
304
|
+
this.#emptyPromise = new Promise(r => (this.#emptyResolve = r));
|
|
305
|
+
}
|
|
306
|
+
return this.#emptyPromise;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* 内部:每次数据变动后,检查哪些 waiter 已满足 */
|
|
310
|
+
#wakeWaiters() {
|
|
311
|
+
for (let i = this.#waiters.length - 1; i >= 0; i--) {
|
|
312
|
+
const w = this.#waiters[i];
|
|
313
|
+
if (this.#items.length >= w.min) {
|
|
314
|
+
this.#removeWaiter(w);
|
|
315
|
+
w.resolve(this.#flush());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#removeWaiter(waiter) {
|
|
321
|
+
const idx = this.#waiters.indexOf(waiter);
|
|
322
|
+
if (idx !== -1) {
|
|
323
|
+
this.#waiters.splice(idx, 1);
|
|
324
|
+
if (waiter.timer) clearTimeout(waiter.timer);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#flush() {
|
|
329
|
+
const snapshot = [...this.#items];
|
|
330
|
+
this.#items.length = 0;
|
|
331
|
+
return snapshot;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* 当前缓存长度(不含等待者) */
|
|
335
|
+
get length() {
|
|
336
|
+
return this.#items.length;
|
|
337
|
+
}
|
|
278
338
|
}
|
|
339
|
+
;// CONCATENATED MODULE: ./src/utils/stream-context.js
|
|
279
340
|
|
|
280
|
-
/**
|
|
281
|
-
* 渲染工具列表
|
|
282
|
-
*/
|
|
283
|
-
function renderMcpTools() {
|
|
284
|
-
const container = document.getElementById("mcpToolsContainer");
|
|
285
|
-
const countSpan = document.getElementById("mcpToolsCount");
|
|
286
341
|
|
|
287
|
-
|
|
342
|
+
// 音频流播放上下文类
|
|
343
|
+
class StreamingContext {
|
|
344
|
+
constructor(
|
|
345
|
+
opusDecoder,
|
|
346
|
+
audioContext,
|
|
347
|
+
sampleRate,
|
|
348
|
+
channels,
|
|
349
|
+
minAudioDuration
|
|
350
|
+
) {
|
|
351
|
+
this.opusDecoder = opusDecoder;
|
|
352
|
+
this.audioContext = audioContext;
|
|
288
353
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
354
|
+
// 音频参数
|
|
355
|
+
this.sampleRate = sampleRate;
|
|
356
|
+
this.channels = channels;
|
|
357
|
+
this.minAudioDuration = minAudioDuration;
|
|
358
|
+
|
|
359
|
+
// 初始化队列和状态
|
|
360
|
+
this.queue = []; // 已解码的PCM队列。正在播放
|
|
361
|
+
this.activeQueue = new BlockingQueue(); // 已解码的PCM队列。准备播放
|
|
362
|
+
this.pendingAudioBufferQueue = []; // 待处理的缓存队列
|
|
363
|
+
this.audioBufferQueue = new BlockingQueue(); // 缓存队列
|
|
364
|
+
this.playing = false; // 是否正在播放
|
|
365
|
+
this.endOfStream = false; // 是否收到结束信号
|
|
366
|
+
this.source = null; // 当前音频源
|
|
367
|
+
this.totalSamples = 0; // 累积的总样本数
|
|
368
|
+
this.lastPlayTime = 0; // 上次播放的时间戳
|
|
293
369
|
}
|
|
294
370
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
: 0;
|
|
300
|
-
const requiredCount = tool.inputSchema.required
|
|
301
|
-
? tool.inputSchema.required.length
|
|
302
|
-
: 0;
|
|
303
|
-
const hasMockResponse =
|
|
304
|
-
tool.mockResponse && Object.keys(tool.mockResponse).length > 0;
|
|
371
|
+
// 缓存音频数组
|
|
372
|
+
pushAudioBuffer(item) {
|
|
373
|
+
this.audioBufferQueue.enqueue(...item);
|
|
374
|
+
}
|
|
305
375
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
</button>
|
|
315
|
-
<button onclick="window.mcpModule.deleteMcpTool(${index})"
|
|
316
|
-
style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #f44336; color: white; cursor: pointer; font-size: 12px;">
|
|
317
|
-
🗑️ 删除
|
|
318
|
-
</button>
|
|
319
|
-
</div>
|
|
320
|
-
</div>
|
|
321
|
-
<div class="mcp-tool-description">${tool.description}</div>
|
|
322
|
-
<div class="mcp-tool-info">
|
|
323
|
-
<div class="mcp-tool-info-row">
|
|
324
|
-
<span class="mcp-tool-info-label">参数数量:</span>
|
|
325
|
-
<span class="mcp-tool-info-value">${paramCount} 个 ${
|
|
326
|
-
requiredCount > 0 ? `(${requiredCount} 个必填)` : ""
|
|
327
|
-
}</span>
|
|
328
|
-
</div>
|
|
329
|
-
<div class="mcp-tool-info-row">
|
|
330
|
-
<span class="mcp-tool-info-label">模拟返回:</span>
|
|
331
|
-
<span class="mcp-tool-info-value">${
|
|
332
|
-
hasMockResponse
|
|
333
|
-
? "✅ 已配置: " + JSON.stringify(tool.mockResponse)
|
|
334
|
-
: "⚪ 使用默认"
|
|
335
|
-
}</span>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
`;
|
|
340
|
-
})
|
|
341
|
-
.join("");
|
|
342
|
-
}
|
|
376
|
+
// 获取需要处理缓存队列,单线程:在audioBufferQueue一直更新的状态下不会出现安全问题
|
|
377
|
+
async getPendingAudioBufferQueue() {
|
|
378
|
+
// 原子交换 + 清空
|
|
379
|
+
[this.pendingAudioBufferQueue, this.audioBufferQueue] = [
|
|
380
|
+
await this.audioBufferQueue.dequeue(),
|
|
381
|
+
new BlockingQueue(),
|
|
382
|
+
];
|
|
383
|
+
}
|
|
343
384
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
385
|
+
// 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题
|
|
386
|
+
async getQueue(minSamples) {
|
|
387
|
+
let TepArray = [];
|
|
388
|
+
const num =
|
|
389
|
+
minSamples - this.queue.length > 0 ? minSamples - this.queue.length : 1;
|
|
390
|
+
// 原子交换 + 清空
|
|
391
|
+
[TepArray, this.activeQueue] = [
|
|
392
|
+
await this.activeQueue.dequeue(num),
|
|
393
|
+
new BlockingQueue(),
|
|
394
|
+
];
|
|
395
|
+
this.queue.push(...TepArray);
|
|
396
|
+
}
|
|
349
397
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
398
|
+
// 将Int16音频数据转换为Float32音频数据
|
|
399
|
+
convertInt16ToFloat32(int16Data) {
|
|
400
|
+
const float32Data = new Float32Array(int16Data.length);
|
|
401
|
+
for (let i = 0; i < int16Data.length; i++) {
|
|
402
|
+
// 将[-32768,32767]范围转换为[-1,1],统一使用32768.0避免不对称失真
|
|
403
|
+
float32Data[i] = int16Data[i] / 32768.0;
|
|
404
|
+
}
|
|
405
|
+
return float32Data;
|
|
354
406
|
}
|
|
355
407
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
删除
|
|
365
|
-
</button>
|
|
366
|
-
</div>
|
|
367
|
-
<div class="mcp-property-row">
|
|
368
|
-
<div>
|
|
369
|
-
<label class="mcp-small-label">参数名称 *</label>
|
|
370
|
-
<input type="text" class="mcp-small-input" value="${
|
|
371
|
-
prop.name
|
|
372
|
-
}"
|
|
373
|
-
onchange="window.mcpModule.updateMcpProperty(${index}, 'name', this.value)" required>
|
|
374
|
-
</div>
|
|
375
|
-
<div>
|
|
376
|
-
<label class="mcp-small-label">数据类型 *</label>
|
|
377
|
-
<select class="mcp-small-input" onchange="window.mcpModule.updateMcpProperty(${index}, 'type', this.value)">
|
|
378
|
-
<option value="string" ${
|
|
379
|
-
prop.type === "string" ? "selected" : ""
|
|
380
|
-
}>字符串</option>
|
|
381
|
-
<option value="integer" ${
|
|
382
|
-
prop.type === "integer" ? "selected" : ""
|
|
383
|
-
}>整数</option>
|
|
384
|
-
<option value="number" ${
|
|
385
|
-
prop.type === "number" ? "selected" : ""
|
|
386
|
-
}>数字</option>
|
|
387
|
-
<option value="boolean" ${
|
|
388
|
-
prop.type === "boolean" ? "selected" : ""
|
|
389
|
-
}>布尔值</option>
|
|
390
|
-
<option value="array" ${
|
|
391
|
-
prop.type === "array" ? "selected" : ""
|
|
392
|
-
}>数组</option>
|
|
393
|
-
<option value="object" ${
|
|
394
|
-
prop.type === "object" ? "selected" : ""
|
|
395
|
-
}>对象</option>
|
|
396
|
-
</select>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
399
|
-
${
|
|
400
|
-
prop.type === "integer" || prop.type === "number"
|
|
401
|
-
? `
|
|
402
|
-
<div class="mcp-property-row">
|
|
403
|
-
<div>
|
|
404
|
-
<label class="mcp-small-label">最小值</label>
|
|
405
|
-
<input type="number" class="mcp-small-input" value="${
|
|
406
|
-
prop.minimum !== undefined ? prop.minimum : ""
|
|
407
|
-
}"
|
|
408
|
-
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'minimum', this.value ? parseFloat(this.value) : undefined)">
|
|
409
|
-
</div>
|
|
410
|
-
<div>
|
|
411
|
-
<label class="mcp-small-label">最大值</label>
|
|
412
|
-
<input type="number" class="mcp-small-input" value="${
|
|
413
|
-
prop.maximum !== undefined ? prop.maximum : ""
|
|
414
|
-
}"
|
|
415
|
-
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'maximum', this.value ? parseFloat(this.value) : undefined)">
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
`
|
|
419
|
-
: ""
|
|
420
|
-
}
|
|
421
|
-
<div class="mcp-property-row-full">
|
|
422
|
-
<label class="mcp-small-label">参数描述</label>
|
|
423
|
-
<input type="text" class="mcp-small-input" value="${
|
|
424
|
-
prop.description || ""
|
|
425
|
-
}"
|
|
426
|
-
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'description', this.value)">
|
|
427
|
-
</div>
|
|
428
|
-
<label class="mcp-checkbox-label">
|
|
429
|
-
<input type="checkbox" ${prop.required ? "checked" : ""}
|
|
430
|
-
onchange="window.mcpModule.updateMcpProperty(${index}, 'required', this.checked)">
|
|
431
|
-
必填参数
|
|
432
|
-
</label>
|
|
433
|
-
</div>
|
|
434
|
-
`
|
|
435
|
-
)
|
|
436
|
-
.join("");
|
|
437
|
-
}
|
|
408
|
+
// 将Opus数据解码为PCM
|
|
409
|
+
async decodeOpusFrames() {
|
|
410
|
+
if (!this.opusDecoder) {
|
|
411
|
+
console.log("Opus解码器未初始化,无法解码", "error");
|
|
412
|
+
return;
|
|
413
|
+
} else {
|
|
414
|
+
console.log("Opus解码器启动", "info");
|
|
415
|
+
}
|
|
438
416
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
417
|
+
while (true) {
|
|
418
|
+
let decodedSamples = [];
|
|
419
|
+
for (const frame of this.pendingAudioBufferQueue) {
|
|
420
|
+
try {
|
|
421
|
+
// 使用Opus解码器解码
|
|
422
|
+
const frameData = this.opusDecoder.decode(frame);
|
|
423
|
+
if (frameData && frameData.length > 0) {
|
|
424
|
+
// 转换为Float32
|
|
425
|
+
const floatData = this.convertInt16ToFloat32(frameData);
|
|
426
|
+
// 使用循环替代展开运算符
|
|
427
|
+
for (let i = 0; i < floatData.length; i++) {
|
|
428
|
+
decodedSamples.push(floatData[i]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.log("Opus解码失败: " + error.message, "error");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
451
435
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
renderMcpProperties();
|
|
463
|
-
return;
|
|
436
|
+
if (decodedSamples.length > 0) {
|
|
437
|
+
// 使用循环替代展开运算符
|
|
438
|
+
for (let i = 0; i < decodedSamples.length; i++) {
|
|
439
|
+
this.activeQueue.enqueue(decodedSamples[i]);
|
|
440
|
+
}
|
|
441
|
+
this.totalSamples += decodedSamples.length;
|
|
442
|
+
} else {
|
|
443
|
+
console.log("没有成功解码的样本", "warning");
|
|
444
|
+
}
|
|
445
|
+
await this.getPendingAudioBufferQueue();
|
|
464
446
|
}
|
|
465
447
|
}
|
|
466
448
|
|
|
467
|
-
|
|
449
|
+
// 开始播放音频
|
|
450
|
+
async startPlaying() {
|
|
451
|
+
let scheduledEndTime = this.audioContext.currentTime; // 跟踪已调度音频的结束时间
|
|
468
452
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
453
|
+
while (true) {
|
|
454
|
+
// 初始缓冲:等待足够的样本再开始播放
|
|
455
|
+
const minSamples = this.sampleRate * this.minAudioDuration * 2;
|
|
456
|
+
if (!this.playing && this.queue.length < minSamples) {
|
|
457
|
+
await this.getQueue(minSamples);
|
|
458
|
+
}
|
|
459
|
+
this.playing = true;
|
|
475
460
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
461
|
+
// 持续播放队列中的音频,每次播放一个小块
|
|
462
|
+
while (this.playing && this.queue.length > 0) {
|
|
463
|
+
// 每次播放120ms的音频(2个Opus包)
|
|
464
|
+
const playDuration = 0.12;
|
|
465
|
+
const targetSamples = Math.floor(this.sampleRate * playDuration);
|
|
466
|
+
const actualSamples = Math.min(this.queue.length, targetSamples);
|
|
483
467
|
|
|
484
|
-
|
|
485
|
-
* 设置事件监听
|
|
486
|
-
*/
|
|
487
|
-
function setupMcpEventListeners() {
|
|
488
|
-
const toggleBtn = document.getElementById("toggleMcpTools");
|
|
489
|
-
const panel = document.getElementById("mcpToolsPanel");
|
|
490
|
-
const addBtn = document.getElementById("addMcpToolBtn");
|
|
491
|
-
const modal = document.getElementById("mcpToolModal");
|
|
492
|
-
const closeBtn = document.getElementById("closeMcpModalBtn");
|
|
493
|
-
const cancelBtn = document.getElementById("cancelMcpBtn");
|
|
494
|
-
const form = document.getElementById("mcpToolForm");
|
|
495
|
-
const addPropertyBtn = document.getElementById("addMcpPropertyBtn");
|
|
468
|
+
if (actualSamples === 0) break;
|
|
496
469
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
470
|
+
const currentSamples = this.queue.splice(0, actualSamples);
|
|
471
|
+
const audioBuffer = this.audioContext.createBuffer(
|
|
472
|
+
this.channels,
|
|
473
|
+
currentSamples.length,
|
|
474
|
+
this.sampleRate
|
|
475
|
+
);
|
|
476
|
+
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
|
|
502
477
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
addPropertyBtn.addEventListener("click", addMcpProperty);
|
|
478
|
+
// 创建音频源
|
|
479
|
+
this.source = this.audioContext.createBufferSource();
|
|
480
|
+
this.source.buffer = audioBuffer;
|
|
507
481
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
482
|
+
// 精确调度播放时间
|
|
483
|
+
const currentTime = this.audioContext.currentTime;
|
|
484
|
+
const startTime = Math.max(scheduledEndTime, currentTime);
|
|
511
485
|
|
|
512
|
-
|
|
513
|
-
|
|
486
|
+
// 直接连接到输出
|
|
487
|
+
this.source.connect(this.audioContext.destination);
|
|
514
488
|
|
|
515
|
-
|
|
516
|
-
* 打开模态框
|
|
517
|
-
*/
|
|
518
|
-
function openMcpModal(index = null) {
|
|
519
|
-
const isConnected = websocket && websocket.readyState === WebSocket.OPEN;
|
|
520
|
-
if (isConnected) {
|
|
521
|
-
alert("WebSocket 已连接,无法编辑工具");
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
489
|
+
this.source.start(startTime);
|
|
524
490
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
491
|
+
// 更新下一个音频块的调度时间
|
|
492
|
+
const duration = audioBuffer.duration;
|
|
493
|
+
scheduledEndTime = startTime + duration;
|
|
494
|
+
this.lastPlayTime = startTime;
|
|
528
495
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
document.getElementById("mcpMockResponse").value = tool.mockResponse
|
|
535
|
-
? JSON.stringify(tool.mockResponse, null, 2)
|
|
536
|
-
: "";
|
|
496
|
+
// 如果队列中数据不足,等待新数据
|
|
497
|
+
if (this.queue.length < targetSamples) {
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
537
501
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (schema.properties) {
|
|
541
|
-
Object.keys(schema.properties).forEach((key) => {
|
|
542
|
-
const prop = schema.properties[key];
|
|
543
|
-
mcpProperties.push({
|
|
544
|
-
name: key,
|
|
545
|
-
type: prop.type || "string",
|
|
546
|
-
minimum: prop.minimum,
|
|
547
|
-
maximum: prop.maximum,
|
|
548
|
-
description: prop.description || "",
|
|
549
|
-
required: schema.required && schema.required.includes(key),
|
|
550
|
-
});
|
|
551
|
-
});
|
|
502
|
+
// 等待新数据
|
|
503
|
+
await this.getQueue(minSamples);
|
|
552
504
|
}
|
|
553
|
-
} else {
|
|
554
|
-
document.getElementById("mcpModalTitle").textContent = "添加工具";
|
|
555
|
-
document.getElementById("mcpToolForm").reset();
|
|
556
|
-
mcpProperties = [];
|
|
557
505
|
}
|
|
558
|
-
|
|
559
|
-
renderMcpProperties();
|
|
560
|
-
document.getElementById("mcpToolModal").style.display = "block";
|
|
561
506
|
}
|
|
562
507
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
508
|
+
// 创建streamingContext实例的工厂函数
|
|
509
|
+
function createStreamingContext(
|
|
510
|
+
opusDecoder,
|
|
511
|
+
audioContext,
|
|
512
|
+
sampleRate,
|
|
513
|
+
channels,
|
|
514
|
+
minAudioDuration
|
|
515
|
+
) {
|
|
516
|
+
return new StreamingContext(
|
|
517
|
+
opusDecoder,
|
|
518
|
+
audioContext,
|
|
519
|
+
sampleRate,
|
|
520
|
+
channels,
|
|
521
|
+
minAudioDuration
|
|
522
|
+
);
|
|
572
523
|
}
|
|
573
524
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
*/
|
|
577
|
-
function handleMcpSubmit(e) {
|
|
578
|
-
e.preventDefault();
|
|
579
|
-
const errorContainer = document.getElementById("mcpErrorContainer");
|
|
580
|
-
errorContainer.innerHTML = "";
|
|
525
|
+
;// CONCATENATED MODULE: ./src/utils/player.js
|
|
526
|
+
// 音频播放模块
|
|
581
527
|
|
|
582
|
-
const name = document.getElementById("mcpToolName").value.trim();
|
|
583
|
-
const description = document
|
|
584
|
-
.getElementById("mcpToolDescription")
|
|
585
|
-
.value.trim();
|
|
586
|
-
const mockResponseText = document
|
|
587
|
-
.getElementById("mcpMockResponse")
|
|
588
|
-
.value.trim();
|
|
589
528
|
|
|
590
|
-
// 检查名称重复
|
|
591
|
-
const isDuplicate = mcpTools.some(
|
|
592
|
-
(tool, index) => tool.name === name && index !== mcpEditingIndex
|
|
593
|
-
);
|
|
594
529
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
530
|
+
// 音频播放器类
|
|
531
|
+
class AudioPlayer {
|
|
532
|
+
constructor() {
|
|
533
|
+
// 音频参数
|
|
534
|
+
this.SAMPLE_RATE = 16000;
|
|
535
|
+
this.CHANNELS = 1;
|
|
536
|
+
this.FRAME_SIZE = 960;
|
|
537
|
+
this.MIN_AUDIO_DURATION = 0.12;
|
|
538
|
+
|
|
539
|
+
// 状态
|
|
540
|
+
this.audioContext = null;
|
|
541
|
+
this.opusDecoder = null;
|
|
542
|
+
this.streamingContext = null;
|
|
543
|
+
this.queue = new BlockingQueue();
|
|
544
|
+
this.isPlaying = false;
|
|
598
545
|
}
|
|
599
546
|
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
547
|
+
// 获取或创建AudioContext
|
|
548
|
+
getAudioContext() {
|
|
549
|
+
if (!this.audioContext) {
|
|
550
|
+
this.audioContext = new (window.AudioContext ||
|
|
551
|
+
window.webkitAudioContext)({
|
|
552
|
+
sampleRate: this.SAMPLE_RATE,
|
|
553
|
+
latencyHint: "interactive",
|
|
554
|
+
});
|
|
555
|
+
console.log(
|
|
556
|
+
"创建音频上下文,采样率: " + this.SAMPLE_RATE + "Hz",
|
|
557
|
+
"debug"
|
|
558
|
+
);
|
|
608
559
|
}
|
|
560
|
+
return this.audioContext;
|
|
609
561
|
}
|
|
610
562
|
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
properties: {},
|
|
615
|
-
required: [],
|
|
616
|
-
};
|
|
563
|
+
// 初始化Opus解码器
|
|
564
|
+
async initOpusDecoder() {
|
|
565
|
+
if (this.opusDecoder) return this.opusDecoder;
|
|
617
566
|
|
|
618
|
-
|
|
619
|
-
|
|
567
|
+
try {
|
|
568
|
+
if (typeof window.ModuleInstance === "undefined") {
|
|
569
|
+
if (typeof Module !== "undefined") {
|
|
570
|
+
window.ModuleInstance = Module;
|
|
571
|
+
console.log("使用全局Module作为ModuleInstance", "info");
|
|
572
|
+
} else {
|
|
573
|
+
throw new Error("Opus库未加载,ModuleInstance和Module对象都不存在");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
620
576
|
|
|
621
|
-
|
|
622
|
-
propSchema.description = prop.description;
|
|
623
|
-
}
|
|
577
|
+
const mod = window.ModuleInstance;
|
|
624
578
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
}
|
|
579
|
+
this.opusDecoder = {
|
|
580
|
+
channels: this.CHANNELS,
|
|
581
|
+
rate: this.SAMPLE_RATE,
|
|
582
|
+
frameSize: this.FRAME_SIZE,
|
|
583
|
+
module: mod,
|
|
584
|
+
decoderPtr: null,
|
|
633
585
|
|
|
634
|
-
|
|
586
|
+
init: function () {
|
|
587
|
+
if (this.decoderPtr) return true;
|
|
635
588
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
});
|
|
589
|
+
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
|
590
|
+
console.log(`Opus解码器大小: ${decoderSize}字节`, "debug");
|
|
640
591
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
592
|
+
this.decoderPtr = mod._malloc(decoderSize);
|
|
593
|
+
if (!this.decoderPtr) {
|
|
594
|
+
throw new Error("无法分配解码器内存");
|
|
595
|
+
}
|
|
644
596
|
|
|
645
|
-
|
|
597
|
+
const err = mod._opus_decoder_init(
|
|
598
|
+
this.decoderPtr,
|
|
599
|
+
this.rate,
|
|
600
|
+
this.channels
|
|
601
|
+
);
|
|
646
602
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
mcpTools.push(tool);
|
|
652
|
-
console.log(`已添加工具: ${name}`, "success");
|
|
653
|
-
}
|
|
603
|
+
if (err < 0) {
|
|
604
|
+
this.destroy();
|
|
605
|
+
throw new Error(`Opus解码器初始化失败: ${err}`);
|
|
606
|
+
}
|
|
654
607
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
608
|
+
console.log("Opus解码器初始化成功", "success");
|
|
609
|
+
return true;
|
|
610
|
+
},
|
|
659
611
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
612
|
+
decode: function (opusData) {
|
|
613
|
+
if (!this.decoderPtr) {
|
|
614
|
+
if (!this.init()) {
|
|
615
|
+
throw new Error("解码器未初始化且无法初始化");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
667
618
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
*/
|
|
671
|
-
function editMcpTool(index) {
|
|
672
|
-
openMcpModal(index);
|
|
673
|
-
}
|
|
619
|
+
try {
|
|
620
|
+
const mod = this.module;
|
|
674
621
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
*/
|
|
678
|
-
function deleteMcpTool(index) {
|
|
679
|
-
const isConnected = websocket && websocket.readyState === WebSocket.OPEN;
|
|
680
|
-
if (isConnected) {
|
|
681
|
-
alert("WebSocket 已连接,无法编辑工具");
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
if (confirm(`确定要删除工具 "${mcpTools[index].name}" 吗?`)) {
|
|
685
|
-
const toolName = mcpTools[index].name;
|
|
686
|
-
mcpTools.splice(index, 1);
|
|
687
|
-
saveMcpTools();
|
|
688
|
-
renderMcpTools();
|
|
689
|
-
console.log(`已删除工具: ${toolName}`, "info");
|
|
690
|
-
}
|
|
691
|
-
}
|
|
622
|
+
const opusPtr = mod._malloc(opusData.length);
|
|
623
|
+
mod.HEAPU8.set(opusData, opusPtr);
|
|
692
624
|
|
|
693
|
-
|
|
694
|
-
* 保存工具
|
|
695
|
-
*/
|
|
696
|
-
function saveMcpTools() {
|
|
697
|
-
localStorage.setItem("mcpTools", JSON.stringify(mcpTools));
|
|
698
|
-
}
|
|
625
|
+
const pcmPtr = mod._malloc(this.frameSize * 2);
|
|
699
626
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}));
|
|
709
|
-
}
|
|
627
|
+
const decodedSamples = mod._opus_decode(
|
|
628
|
+
this.decoderPtr,
|
|
629
|
+
opusPtr,
|
|
630
|
+
opusData.length,
|
|
631
|
+
pcmPtr,
|
|
632
|
+
this.frameSize,
|
|
633
|
+
0
|
|
634
|
+
);
|
|
710
635
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (!tool) {
|
|
717
|
-
console.log(`未找到工具: ${toolName}`, "error");
|
|
718
|
-
return {
|
|
719
|
-
success: false,
|
|
720
|
-
error: `未知工具: ${toolName}`,
|
|
721
|
-
};
|
|
722
|
-
}
|
|
636
|
+
if (decodedSamples < 0) {
|
|
637
|
+
mod._free(opusPtr);
|
|
638
|
+
mod._free(pcmPtr);
|
|
639
|
+
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
|
640
|
+
}
|
|
723
641
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
return {
|
|
729
|
-
success: true,
|
|
730
|
-
message: `工具 ${toolName} 执行成功`,
|
|
731
|
-
data: sessionStorage.getItem('cartList') || [],
|
|
732
|
-
};
|
|
733
|
-
} else if (tool.name == "self.drink_car_reset") {
|
|
734
|
-
console.log("准备触发 resetOrderEvent 事件"); // 增加此日志
|
|
735
|
-
const event = new CustomEvent("resetOrderEvent", {
|
|
736
|
-
detail: toolArgs,
|
|
737
|
-
});
|
|
738
|
-
window.dispatchEvent(event);
|
|
739
|
-
return {
|
|
740
|
-
success: true,
|
|
741
|
-
message: `工具 ${toolName} 执行成功`,
|
|
742
|
-
data: sessionStorage.getItem('cartList') || [],
|
|
743
|
-
}
|
|
744
|
-
} else if (tool.name == "self.drink_order") {
|
|
745
|
-
console.log("准备触发 orderEvent 事件"); // 增加此日志
|
|
746
|
-
const event = new CustomEvent("orderEvent");
|
|
747
|
-
window.dispatchEvent(event);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
642
|
+
const decodedData = new Int16Array(decodedSamples);
|
|
643
|
+
for (let i = 0; i < decodedSamples; i++) {
|
|
644
|
+
decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];
|
|
645
|
+
}
|
|
750
646
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
updateMcpProperty,
|
|
754
|
-
deleteMcpProperty,
|
|
755
|
-
editMcpTool,
|
|
756
|
-
deleteMcpTool,
|
|
757
|
-
};
|
|
647
|
+
mod._free(opusPtr);
|
|
648
|
+
mod._free(pcmPtr);
|
|
758
649
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
650
|
+
return decodedData;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.log(`Opus解码错误: ${error.message}`, "error");
|
|
653
|
+
return new Int16Array(0);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
763
656
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
657
|
+
destroy: function () {
|
|
658
|
+
if (this.decoderPtr) {
|
|
659
|
+
this.module._free(this.decoderPtr);
|
|
660
|
+
this.decoderPtr = null;
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
};
|
|
767
664
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
this.#items.push(item);
|
|
772
|
-
}
|
|
773
|
-
// 如果有额外参数,批量处理所有项
|
|
774
|
-
else {
|
|
775
|
-
const items = [item, ...restItems].filter(i => i);
|
|
776
|
-
if (items.length === 0) return;
|
|
777
|
-
this.#items.push(...items);
|
|
778
|
-
}
|
|
779
|
-
// 若有空队列闸门,一次性放行所有等待者
|
|
780
|
-
if (this.#emptyResolve) {
|
|
781
|
-
this.#emptyResolve();
|
|
782
|
-
this.#emptyResolve = null;
|
|
783
|
-
this.#emptyPromise = null;
|
|
784
|
-
}
|
|
665
|
+
if (!this.opusDecoder.init()) {
|
|
666
|
+
throw new Error("Opus解码器初始化失败");
|
|
667
|
+
}
|
|
785
668
|
|
|
786
|
-
|
|
787
|
-
|
|
669
|
+
return this.opusDecoder;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.log(`Opus解码器初始化失败: ${error.message}`, "error");
|
|
672
|
+
this.opusDecoder = null;
|
|
673
|
+
throw error;
|
|
788
674
|
}
|
|
675
|
+
}
|
|
789
676
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (this.#items.length === 0) {
|
|
794
|
-
await this.#waitForFirstItem();
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// 立即满足
|
|
798
|
-
if (this.#items.length >= min) {
|
|
799
|
-
return this.#flush();
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// 需要等待
|
|
803
|
-
return new Promise((resolve, reject) => {
|
|
804
|
-
let timer = null;
|
|
805
|
-
const waiter = { resolve, reject, min, onTimeout, timer };
|
|
677
|
+
// 启动音频缓冲
|
|
678
|
+
async startAudioBuffering() {
|
|
679
|
+
console.log("开始音频缓冲...", "info");
|
|
806
680
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
this.#removeWaiter(waiter);
|
|
811
|
-
if (onTimeout) onTimeout(this.#items.length);
|
|
812
|
-
resolve(this.#flush());
|
|
813
|
-
}, timeout);
|
|
814
|
-
}
|
|
681
|
+
this.initOpusDecoder().catch((error) => {
|
|
682
|
+
console.log(`预初始化Opus解码器失败: ${error.message}`, "warning");
|
|
683
|
+
});
|
|
815
684
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
685
|
+
const timeout = 400;
|
|
686
|
+
while (true) {
|
|
687
|
+
const packets = await this.queue.dequeue(6, timeout, (count) => {
|
|
688
|
+
console.log(`缓冲超时,当前缓冲包数: ${count},开始播放`, "info");
|
|
689
|
+
});
|
|
690
|
+
if (packets.length) {
|
|
691
|
+
console.log(`已缓冲 ${packets.length} 个音频包,开始播放`, "info");
|
|
692
|
+
this.streamingContext.pushAudioBuffer(packets);
|
|
693
|
+
}
|
|
819
694
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if (
|
|
823
|
-
|
|
695
|
+
while (true) {
|
|
696
|
+
const data = await this.queue.dequeue(99, 30);
|
|
697
|
+
if (data.length) {
|
|
698
|
+
this.streamingContext.pushAudioBuffer(data);
|
|
699
|
+
} else {
|
|
700
|
+
break;
|
|
824
701
|
}
|
|
825
|
-
|
|
702
|
+
}
|
|
826
703
|
}
|
|
704
|
+
}
|
|
827
705
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (this.#items.length >= w.min) {
|
|
833
|
-
this.#removeWaiter(w);
|
|
834
|
-
w.resolve(this.#flush());
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
706
|
+
// 播放已缓冲的音频
|
|
707
|
+
async playBufferedAudio() {
|
|
708
|
+
try {
|
|
709
|
+
this.audioContext = this.getAudioContext();
|
|
838
710
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
711
|
+
if (!this.opusDecoder) {
|
|
712
|
+
console.log("初始化Opus解码器...", "info");
|
|
713
|
+
try {
|
|
714
|
+
this.opusDecoder = await this.initOpusDecoder();
|
|
715
|
+
if (!this.opusDecoder) {
|
|
716
|
+
throw new Error("解码器初始化失败");
|
|
717
|
+
}
|
|
718
|
+
console.log("Opus解码器初始化成功", "success");
|
|
719
|
+
} catch (error) {
|
|
720
|
+
console.log("Opus解码器初始化失败: " + error.message, "error");
|
|
721
|
+
this.isPlaying = false;
|
|
722
|
+
return;
|
|
844
723
|
}
|
|
845
|
-
|
|
724
|
+
}
|
|
846
725
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
726
|
+
if (!this.streamingContext) {
|
|
727
|
+
this.streamingContext = createStreamingContext(
|
|
728
|
+
this.opusDecoder,
|
|
729
|
+
this.audioContext,
|
|
730
|
+
this.SAMPLE_RATE,
|
|
731
|
+
this.CHANNELS,
|
|
732
|
+
this.MIN_AUDIO_DURATION
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
this.streamingContext.decodeOpusFrames();
|
|
737
|
+
this.streamingContext.startPlaying();
|
|
738
|
+
} catch (error) {
|
|
739
|
+
console.log(`播放已缓冲的音频出错: ${error.message}`, "error");
|
|
740
|
+
this.isPlaying = false;
|
|
741
|
+
this.streamingContext = null;
|
|
851
742
|
}
|
|
743
|
+
}
|
|
852
744
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
745
|
+
// 添加音频数据到队列
|
|
746
|
+
enqueueAudioData(opusData) {
|
|
747
|
+
if (opusData.length > 0) {
|
|
748
|
+
this.queue.enqueue(opusData);
|
|
749
|
+
} else {
|
|
750
|
+
console.log("收到空音频数据帧,可能是结束标志", "warning");
|
|
751
|
+
if (this.isPlaying && this.streamingContext) {
|
|
752
|
+
this.streamingContext.endOfStream = true;
|
|
753
|
+
}
|
|
856
754
|
}
|
|
857
|
-
}
|
|
858
|
-
;// CONCATENATED MODULE: ./src/utils/stream-context.js
|
|
755
|
+
}
|
|
859
756
|
|
|
757
|
+
// 预加载解码器
|
|
758
|
+
async preload() {
|
|
759
|
+
console.log("预加载Opus解码器...", "info");
|
|
760
|
+
try {
|
|
761
|
+
await this.initOpusDecoder();
|
|
762
|
+
console.log("Opus解码器预加载成功", "success");
|
|
763
|
+
} catch (error) {
|
|
764
|
+
console.log(
|
|
765
|
+
`Opus解码器预加载失败: ${error.message},将在需要时重试`,
|
|
766
|
+
"warning"
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
860
770
|
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
minAudioDuration
|
|
869
|
-
) {
|
|
870
|
-
this.opusDecoder = opusDecoder;
|
|
871
|
-
this.audioContext = audioContext;
|
|
771
|
+
// 启动播放系统
|
|
772
|
+
async start() {
|
|
773
|
+
await this.preload();
|
|
774
|
+
this.playBufferedAudio();
|
|
775
|
+
this.startAudioBuffering();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
872
778
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
this.channels = channels;
|
|
876
|
-
this.minAudioDuration = minAudioDuration;
|
|
779
|
+
// 创建单例
|
|
780
|
+
let audioPlayerInstance = null;
|
|
877
781
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
this.pendingAudioBufferQueue = []; // 待处理的缓存队列
|
|
882
|
-
this.audioBufferQueue = new BlockingQueue(); // 缓存队列
|
|
883
|
-
this.playing = false; // 是否正在播放
|
|
884
|
-
this.endOfStream = false; // 是否收到结束信号
|
|
885
|
-
this.source = null; // 当前音频源
|
|
886
|
-
this.totalSamples = 0; // 累积的总样本数
|
|
887
|
-
this.lastPlayTime = 0; // 上次播放的时间戳
|
|
782
|
+
function getAudioPlayer() {
|
|
783
|
+
if (!audioPlayerInstance) {
|
|
784
|
+
audioPlayerInstance = new AudioPlayer();
|
|
888
785
|
}
|
|
786
|
+
return audioPlayerInstance;
|
|
787
|
+
}
|
|
889
788
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
async getPendingAudioBufferQueue() {
|
|
897
|
-
// 原子交换 + 清空
|
|
898
|
-
[this.pendingAudioBufferQueue, this.audioBufferQueue] = [
|
|
899
|
-
await this.audioBufferQueue.dequeue(),
|
|
900
|
-
new BlockingQueue(),
|
|
901
|
-
];
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// 获取正在播放已解码的PCM队列,单线程:在activeQueue一直更新的状态下不会出现安全问题
|
|
905
|
-
async getQueue(minSamples) {
|
|
906
|
-
let TepArray = [];
|
|
907
|
-
const num =
|
|
908
|
-
minSamples - this.queue.length > 0 ? minSamples - this.queue.length : 1;
|
|
909
|
-
// 原子交换 + 清空
|
|
910
|
-
[TepArray, this.activeQueue] = [
|
|
911
|
-
await this.activeQueue.dequeue(num),
|
|
912
|
-
new BlockingQueue(),
|
|
913
|
-
];
|
|
914
|
-
this.queue.push(...TepArray);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// 将Int16音频数据转换为Float32音频数据
|
|
918
|
-
convertInt16ToFloat32(int16Data) {
|
|
919
|
-
const float32Data = new Float32Array(int16Data.length);
|
|
920
|
-
for (let i = 0; i < int16Data.length; i++) {
|
|
921
|
-
// 将[-32768,32767]范围转换为[-1,1],统一使用32768.0避免不对称失真
|
|
922
|
-
float32Data[i] = int16Data[i] / 32768.0;
|
|
923
|
-
}
|
|
924
|
-
return float32Data;
|
|
925
|
-
}
|
|
789
|
+
;// CONCATENATED MODULE: ./src/utils/tools.js
|
|
790
|
+
// 全局变量
|
|
791
|
+
let mcpTools = [];
|
|
792
|
+
let mcpEditingIndex = null;
|
|
793
|
+
let mcpProperties = [];
|
|
794
|
+
let websocket = null; // 将从外部设置
|
|
926
795
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
796
|
+
/**
|
|
797
|
+
* 设置 WebSocket 实例
|
|
798
|
+
* @param {WebSocket} ws - WebSocket 连接实例
|
|
799
|
+
*/
|
|
800
|
+
function setWebSocket(ws) {
|
|
801
|
+
websocket = ws;
|
|
802
|
+
}
|
|
935
803
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const floatData = this.convertInt16ToFloat32(frameData);
|
|
945
|
-
// 使用循环替代展开运算符
|
|
946
|
-
for (let i = 0; i < floatData.length; i++) {
|
|
947
|
-
decodedSamples.push(floatData[i]);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
} catch (error) {
|
|
951
|
-
console.log("Opus解码失败: " + error.message, "error");
|
|
952
|
-
}
|
|
953
|
-
}
|
|
804
|
+
/**
|
|
805
|
+
* 初始化 MCP 工具
|
|
806
|
+
*/
|
|
807
|
+
async function initMcpTools() {
|
|
808
|
+
// 加载默认工具数据
|
|
809
|
+
const defaultMcpTools = await fetch("/default-mcp-tools.json").then((res) =>
|
|
810
|
+
res.json()
|
|
811
|
+
);
|
|
954
812
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
console.log("没有成功解码的样本", "warning");
|
|
963
|
-
}
|
|
964
|
-
await this.getPendingAudioBufferQueue();
|
|
813
|
+
const savedTools = localStorage.getItem("mcpTools");
|
|
814
|
+
if (savedTools) {
|
|
815
|
+
try {
|
|
816
|
+
mcpTools = JSON.parse(savedTools);
|
|
817
|
+
} catch (e) {
|
|
818
|
+
console.log("加载MCP工具失败,使用默认工具", "warning");
|
|
819
|
+
mcpTools = [...defaultMcpTools];
|
|
965
820
|
}
|
|
821
|
+
} else {
|
|
822
|
+
mcpTools = [...defaultMcpTools];
|
|
966
823
|
}
|
|
967
824
|
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
while (true) {
|
|
973
|
-
// 初始缓冲:等待足够的样本再开始播放
|
|
974
|
-
const minSamples = this.sampleRate * this.minAudioDuration * 2;
|
|
975
|
-
if (!this.playing && this.queue.length < minSamples) {
|
|
976
|
-
await this.getQueue(minSamples);
|
|
977
|
-
}
|
|
978
|
-
this.playing = true;
|
|
979
|
-
|
|
980
|
-
// 持续播放队列中的音频,每次播放一个小块
|
|
981
|
-
while (this.playing && this.queue.length > 0) {
|
|
982
|
-
// 每次播放120ms的音频(2个Opus包)
|
|
983
|
-
const playDuration = 0.12;
|
|
984
|
-
const targetSamples = Math.floor(this.sampleRate * playDuration);
|
|
985
|
-
const actualSamples = Math.min(this.queue.length, targetSamples);
|
|
986
|
-
|
|
987
|
-
if (actualSamples === 0) break;
|
|
988
|
-
|
|
989
|
-
const currentSamples = this.queue.splice(0, actualSamples);
|
|
990
|
-
const audioBuffer = this.audioContext.createBuffer(
|
|
991
|
-
this.channels,
|
|
992
|
-
currentSamples.length,
|
|
993
|
-
this.sampleRate
|
|
994
|
-
);
|
|
995
|
-
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
|
|
825
|
+
// renderMcpTools();
|
|
826
|
+
// setupMcpEventListeners();
|
|
827
|
+
}
|
|
996
828
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
829
|
+
/**
|
|
830
|
+
* 渲染工具列表
|
|
831
|
+
*/
|
|
832
|
+
function renderMcpTools() {
|
|
833
|
+
const container = document.getElementById("mcpToolsContainer");
|
|
834
|
+
const countSpan = document.getElementById("mcpToolsCount");
|
|
1000
835
|
|
|
1001
|
-
|
|
1002
|
-
const currentTime = this.audioContext.currentTime;
|
|
1003
|
-
const startTime = Math.max(scheduledEndTime, currentTime);
|
|
836
|
+
countSpan.textContent = `${mcpTools.length} 个工具`;
|
|
1004
837
|
|
|
1005
|
-
|
|
1006
|
-
|
|
838
|
+
if (mcpTools.length === 0) {
|
|
839
|
+
container.innerHTML =
|
|
840
|
+
'<div style="text-align: center; padding: 30px; color: #999;">暂无工具,点击下方按钮添加新工具</div>';
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
1007
843
|
|
|
1008
|
-
|
|
844
|
+
container.innerHTML = mcpTools
|
|
845
|
+
.map((tool, index) => {
|
|
846
|
+
const paramCount = tool.inputSchema.properties
|
|
847
|
+
? Object.keys(tool.inputSchema.properties).length
|
|
848
|
+
: 0;
|
|
849
|
+
const requiredCount = tool.inputSchema.required
|
|
850
|
+
? tool.inputSchema.required.length
|
|
851
|
+
: 0;
|
|
852
|
+
const hasMockResponse =
|
|
853
|
+
tool.mockResponse && Object.keys(tool.mockResponse).length > 0;
|
|
1009
854
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
855
|
+
return `
|
|
856
|
+
<div class="mcp-tool-card">
|
|
857
|
+
<div class="mcp-tool-header">
|
|
858
|
+
<div class="mcp-tool-name">${tool.name}</div>
|
|
859
|
+
<div class="mcp-tool-actions">
|
|
860
|
+
<button onclick="window.mcpModule.editMcpTool(${index})"
|
|
861
|
+
style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #2196f3; color: white; cursor: pointer; font-size: 12px;">
|
|
862
|
+
✏️ 编辑
|
|
863
|
+
</button>
|
|
864
|
+
<button onclick="window.mcpModule.deleteMcpTool(${index})"
|
|
865
|
+
style="padding: 4px 10px; border: none; border-radius: 4px; background-color: #f44336; color: white; cursor: pointer; font-size: 12px;">
|
|
866
|
+
🗑️ 删除
|
|
867
|
+
</button>
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="mcp-tool-description">${tool.description}</div>
|
|
871
|
+
<div class="mcp-tool-info">
|
|
872
|
+
<div class="mcp-tool-info-row">
|
|
873
|
+
<span class="mcp-tool-info-label">参数数量:</span>
|
|
874
|
+
<span class="mcp-tool-info-value">${paramCount} 个 ${
|
|
875
|
+
requiredCount > 0 ? `(${requiredCount} 个必填)` : ""
|
|
876
|
+
}</span>
|
|
877
|
+
</div>
|
|
878
|
+
<div class="mcp-tool-info-row">
|
|
879
|
+
<span class="mcp-tool-info-label">模拟返回:</span>
|
|
880
|
+
<span class="mcp-tool-info-value">${
|
|
881
|
+
hasMockResponse
|
|
882
|
+
? "✅ 已配置: " + JSON.stringify(tool.mockResponse)
|
|
883
|
+
: "⚪ 使用默认"
|
|
884
|
+
}</span>
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
`;
|
|
889
|
+
})
|
|
890
|
+
.join("");
|
|
891
|
+
}
|
|
1014
892
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
893
|
+
/**
|
|
894
|
+
* 渲染参数列表
|
|
895
|
+
*/
|
|
896
|
+
function renderMcpProperties() {
|
|
897
|
+
const container = document.getElementById("mcpPropertiesContainer");
|
|
1020
898
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
899
|
+
if (mcpProperties.length === 0) {
|
|
900
|
+
container.innerHTML =
|
|
901
|
+
'<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>';
|
|
902
|
+
return;
|
|
1024
903
|
}
|
|
904
|
+
|
|
905
|
+
container.innerHTML = mcpProperties
|
|
906
|
+
.map(
|
|
907
|
+
(prop, index) => `
|
|
908
|
+
<div class="mcp-property-item">
|
|
909
|
+
<div class="mcp-property-header">
|
|
910
|
+
<span class="mcp-property-name">${prop.name}</span>
|
|
911
|
+
<button type="button" onclick="window.mcpModule.deleteMcpProperty(${index})"
|
|
912
|
+
style="padding: 3px 8px; border: none; border-radius: 3px; background-color: #f44336; color: white; cursor: pointer; font-size: 11px;">
|
|
913
|
+
删除
|
|
914
|
+
</button>
|
|
915
|
+
</div>
|
|
916
|
+
<div class="mcp-property-row">
|
|
917
|
+
<div>
|
|
918
|
+
<label class="mcp-small-label">参数名称 *</label>
|
|
919
|
+
<input type="text" class="mcp-small-input" value="${
|
|
920
|
+
prop.name
|
|
921
|
+
}"
|
|
922
|
+
onchange="window.mcpModule.updateMcpProperty(${index}, 'name', this.value)" required>
|
|
923
|
+
</div>
|
|
924
|
+
<div>
|
|
925
|
+
<label class="mcp-small-label">数据类型 *</label>
|
|
926
|
+
<select class="mcp-small-input" onchange="window.mcpModule.updateMcpProperty(${index}, 'type', this.value)">
|
|
927
|
+
<option value="string" ${
|
|
928
|
+
prop.type === "string" ? "selected" : ""
|
|
929
|
+
}>字符串</option>
|
|
930
|
+
<option value="integer" ${
|
|
931
|
+
prop.type === "integer" ? "selected" : ""
|
|
932
|
+
}>整数</option>
|
|
933
|
+
<option value="number" ${
|
|
934
|
+
prop.type === "number" ? "selected" : ""
|
|
935
|
+
}>数字</option>
|
|
936
|
+
<option value="boolean" ${
|
|
937
|
+
prop.type === "boolean" ? "selected" : ""
|
|
938
|
+
}>布尔值</option>
|
|
939
|
+
<option value="array" ${
|
|
940
|
+
prop.type === "array" ? "selected" : ""
|
|
941
|
+
}>数组</option>
|
|
942
|
+
<option value="object" ${
|
|
943
|
+
prop.type === "object" ? "selected" : ""
|
|
944
|
+
}>对象</option>
|
|
945
|
+
</select>
|
|
946
|
+
</div>
|
|
947
|
+
</div>
|
|
948
|
+
${
|
|
949
|
+
prop.type === "integer" || prop.type === "number"
|
|
950
|
+
? `
|
|
951
|
+
<div class="mcp-property-row">
|
|
952
|
+
<div>
|
|
953
|
+
<label class="mcp-small-label">最小值</label>
|
|
954
|
+
<input type="number" class="mcp-small-input" value="${
|
|
955
|
+
prop.minimum !== undefined ? prop.minimum : ""
|
|
956
|
+
}"
|
|
957
|
+
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'minimum', this.value ? parseFloat(this.value) : undefined)">
|
|
958
|
+
</div>
|
|
959
|
+
<div>
|
|
960
|
+
<label class="mcp-small-label">最大值</label>
|
|
961
|
+
<input type="number" class="mcp-small-input" value="${
|
|
962
|
+
prop.maximum !== undefined ? prop.maximum : ""
|
|
963
|
+
}"
|
|
964
|
+
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'maximum', this.value ? parseFloat(this.value) : undefined)">
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
`
|
|
968
|
+
: ""
|
|
969
|
+
}
|
|
970
|
+
<div class="mcp-property-row-full">
|
|
971
|
+
<label class="mcp-small-label">参数描述</label>
|
|
972
|
+
<input type="text" class="mcp-small-input" value="${
|
|
973
|
+
prop.description || ""
|
|
974
|
+
}"
|
|
975
|
+
placeholder="可选" onchange="window.mcpModule.updateMcpProperty(${index}, 'description', this.value)">
|
|
976
|
+
</div>
|
|
977
|
+
<label class="mcp-checkbox-label">
|
|
978
|
+
<input type="checkbox" ${prop.required ? "checked" : ""}
|
|
979
|
+
onchange="window.mcpModule.updateMcpProperty(${index}, 'required', this.checked)">
|
|
980
|
+
必填参数
|
|
981
|
+
</label>
|
|
982
|
+
</div>
|
|
983
|
+
`
|
|
984
|
+
)
|
|
985
|
+
.join("");
|
|
1025
986
|
}
|
|
1026
987
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
sampleRate,
|
|
1039
|
-
channels,
|
|
1040
|
-
minAudioDuration
|
|
1041
|
-
);
|
|
988
|
+
/**
|
|
989
|
+
* 添加参数
|
|
990
|
+
*/
|
|
991
|
+
function addMcpProperty() {
|
|
992
|
+
mcpProperties.push({
|
|
993
|
+
name: `param_${mcpProperties.length + 1}`,
|
|
994
|
+
type: "string",
|
|
995
|
+
required: false,
|
|
996
|
+
description: "",
|
|
997
|
+
});
|
|
998
|
+
renderMcpProperties();
|
|
1042
999
|
}
|
|
1043
1000
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
this.MIN_AUDIO_DURATION = 0.12;
|
|
1057
|
-
|
|
1058
|
-
// 状态
|
|
1059
|
-
this.audioContext = null;
|
|
1060
|
-
this.opusDecoder = null;
|
|
1061
|
-
this.streamingContext = null;
|
|
1062
|
-
this.queue = new BlockingQueue();
|
|
1063
|
-
this.isPlaying = false;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// 获取或创建AudioContext
|
|
1067
|
-
getAudioContext() {
|
|
1068
|
-
if (!this.audioContext) {
|
|
1069
|
-
this.audioContext = new (window.AudioContext ||
|
|
1070
|
-
window.webkitAudioContext)({
|
|
1071
|
-
sampleRate: this.SAMPLE_RATE,
|
|
1072
|
-
latencyHint: "interactive",
|
|
1073
|
-
});
|
|
1074
|
-
console.log(
|
|
1075
|
-
"创建音频上下文,采样率: " + this.SAMPLE_RATE + "Hz",
|
|
1076
|
-
"debug"
|
|
1077
|
-
);
|
|
1001
|
+
/**
|
|
1002
|
+
* 更新参数
|
|
1003
|
+
*/
|
|
1004
|
+
function updateMcpProperty(index, field, value) {
|
|
1005
|
+
if (field === "name") {
|
|
1006
|
+
const isDuplicate = mcpProperties.some(
|
|
1007
|
+
(p, i) => i !== index && p.name === value
|
|
1008
|
+
);
|
|
1009
|
+
if (isDuplicate) {
|
|
1010
|
+
alert("参数名称已存在,请使用不同的名称");
|
|
1011
|
+
renderMcpProperties();
|
|
1012
|
+
return;
|
|
1078
1013
|
}
|
|
1079
|
-
return this.audioContext;
|
|
1080
1014
|
}
|
|
1081
1015
|
|
|
1082
|
-
|
|
1083
|
-
async initOpusDecoder() {
|
|
1084
|
-
if (this.opusDecoder) return this.opusDecoder;
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
if (typeof window.ModuleInstance === "undefined") {
|
|
1088
|
-
if (typeof Module !== "undefined") {
|
|
1089
|
-
window.ModuleInstance = Module;
|
|
1090
|
-
console.log("使用全局Module作为ModuleInstance", "info");
|
|
1091
|
-
} else {
|
|
1092
|
-
throw new Error("Opus库未加载,ModuleInstance和Module对象都不存在");
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const mod = window.ModuleInstance;
|
|
1097
|
-
|
|
1098
|
-
this.opusDecoder = {
|
|
1099
|
-
channels: this.CHANNELS,
|
|
1100
|
-
rate: this.SAMPLE_RATE,
|
|
1101
|
-
frameSize: this.FRAME_SIZE,
|
|
1102
|
-
module: mod,
|
|
1103
|
-
decoderPtr: null,
|
|
1016
|
+
mcpProperties[index][field] = value;
|
|
1104
1017
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1018
|
+
if (field === "type" && value !== "integer" && value !== "number") {
|
|
1019
|
+
delete mcpProperties[index].minimum;
|
|
1020
|
+
delete mcpProperties[index].maximum;
|
|
1021
|
+
renderMcpProperties();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1107
1024
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1025
|
+
/**
|
|
1026
|
+
* 删除参数
|
|
1027
|
+
*/
|
|
1028
|
+
function deleteMcpProperty(index) {
|
|
1029
|
+
mcpProperties.splice(index, 1);
|
|
1030
|
+
renderMcpProperties();
|
|
1031
|
+
}
|
|
1110
1032
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1033
|
+
/**
|
|
1034
|
+
* 设置事件监听
|
|
1035
|
+
*/
|
|
1036
|
+
function setupMcpEventListeners() {
|
|
1037
|
+
const toggleBtn = document.getElementById("toggleMcpTools");
|
|
1038
|
+
const panel = document.getElementById("mcpToolsPanel");
|
|
1039
|
+
const addBtn = document.getElementById("addMcpToolBtn");
|
|
1040
|
+
const modal = document.getElementById("mcpToolModal");
|
|
1041
|
+
const closeBtn = document.getElementById("closeMcpModalBtn");
|
|
1042
|
+
const cancelBtn = document.getElementById("cancelMcpBtn");
|
|
1043
|
+
const form = document.getElementById("mcpToolForm");
|
|
1044
|
+
const addPropertyBtn = document.getElementById("addMcpPropertyBtn");
|
|
1115
1045
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1046
|
+
toggleBtn.addEventListener("click", () => {
|
|
1047
|
+
const isExpanded = panel.classList.contains("expanded");
|
|
1048
|
+
panel.classList.toggle("expanded");
|
|
1049
|
+
toggleBtn.textContent = isExpanded ? "展开" : "收起";
|
|
1050
|
+
});
|
|
1121
1051
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1052
|
+
addBtn.addEventListener("click", () => openMcpModal());
|
|
1053
|
+
closeBtn.addEventListener("click", closeMcpModal);
|
|
1054
|
+
cancelBtn.addEventListener("click", closeMcpModal);
|
|
1055
|
+
addPropertyBtn.addEventListener("click", addMcpProperty);
|
|
1126
1056
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1057
|
+
modal.addEventListener("click", (e) => {
|
|
1058
|
+
if (e.target === modal) closeMcpModal();
|
|
1059
|
+
});
|
|
1130
1060
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
if (!this.init()) {
|
|
1134
|
-
throw new Error("解码器未初始化且无法初始化");
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1061
|
+
form.addEventListener("submit", handleMcpSubmit);
|
|
1062
|
+
}
|
|
1137
1063
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* 打开模态框
|
|
1066
|
+
*/
|
|
1067
|
+
function openMcpModal(index = null) {
|
|
1068
|
+
const isConnected = websocket && websocket.readyState === WebSocket.OPEN;
|
|
1069
|
+
if (isConnected) {
|
|
1070
|
+
alert("WebSocket 已连接,无法编辑工具");
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1140
1073
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1074
|
+
mcpEditingIndex = index;
|
|
1075
|
+
const errorContainer = document.getElementById("mcpErrorContainer");
|
|
1076
|
+
errorContainer.innerHTML = "";
|
|
1143
1077
|
|
|
1144
|
-
|
|
1078
|
+
if (index !== null) {
|
|
1079
|
+
document.getElementById("mcpModalTitle").textContent = "编辑工具";
|
|
1080
|
+
const tool = mcpTools[index];
|
|
1081
|
+
document.getElementById("mcpToolName").value = tool.name;
|
|
1082
|
+
document.getElementById("mcpToolDescription").value = tool.description;
|
|
1083
|
+
document.getElementById("mcpMockResponse").value = tool.mockResponse
|
|
1084
|
+
? JSON.stringify(tool.mockResponse, null, 2)
|
|
1085
|
+
: "";
|
|
1145
1086
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1087
|
+
mcpProperties = [];
|
|
1088
|
+
const schema = tool.inputSchema;
|
|
1089
|
+
if (schema.properties) {
|
|
1090
|
+
Object.keys(schema.properties).forEach((key) => {
|
|
1091
|
+
const prop = schema.properties[key];
|
|
1092
|
+
mcpProperties.push({
|
|
1093
|
+
name: key,
|
|
1094
|
+
type: prop.type || "string",
|
|
1095
|
+
minimum: prop.minimum,
|
|
1096
|
+
maximum: prop.maximum,
|
|
1097
|
+
description: prop.description || "",
|
|
1098
|
+
required: schema.required && schema.required.includes(key),
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
document.getElementById("mcpModalTitle").textContent = "添加工具";
|
|
1104
|
+
document.getElementById("mcpToolForm").reset();
|
|
1105
|
+
mcpProperties = [];
|
|
1106
|
+
}
|
|
1154
1107
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
|
1159
|
-
}
|
|
1108
|
+
renderMcpProperties();
|
|
1109
|
+
document.getElementById("mcpToolModal").style.display = "block";
|
|
1110
|
+
}
|
|
1160
1111
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1112
|
+
/**
|
|
1113
|
+
* 关闭模态框
|
|
1114
|
+
*/
|
|
1115
|
+
function closeMcpModal() {
|
|
1116
|
+
document.getElementById("mcpToolModal").style.display = "none";
|
|
1117
|
+
mcpEditingIndex = null;
|
|
1118
|
+
document.getElementById("mcpToolForm").reset();
|
|
1119
|
+
mcpProperties = [];
|
|
1120
|
+
document.getElementById("mcpErrorContainer").innerHTML = "";
|
|
1121
|
+
}
|
|
1165
1122
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1123
|
+
/**
|
|
1124
|
+
* 处理表单提交
|
|
1125
|
+
*/
|
|
1126
|
+
function handleMcpSubmit(e) {
|
|
1127
|
+
e.preventDefault();
|
|
1128
|
+
const errorContainer = document.getElementById("mcpErrorContainer");
|
|
1129
|
+
errorContainer.innerHTML = "";
|
|
1168
1130
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1131
|
+
const name = document.getElementById("mcpToolName").value.trim();
|
|
1132
|
+
const description = document
|
|
1133
|
+
.getElementById("mcpToolDescription")
|
|
1134
|
+
.value.trim();
|
|
1135
|
+
const mockResponseText = document
|
|
1136
|
+
.getElementById("mcpMockResponse")
|
|
1137
|
+
.value.trim();
|
|
1175
1138
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
},
|
|
1182
|
-
};
|
|
1139
|
+
// 检查名称重复
|
|
1140
|
+
const isDuplicate = mcpTools.some(
|
|
1141
|
+
(tool, index) => tool.name === name && index !== mcpEditingIndex
|
|
1142
|
+
);
|
|
1183
1143
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1144
|
+
if (isDuplicate) {
|
|
1145
|
+
showMcpError("工具名称已存在,请使用不同的名称");
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1187
1148
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1149
|
+
// 解析模拟返回结果
|
|
1150
|
+
let mockResponse = null;
|
|
1151
|
+
if (mockResponseText) {
|
|
1152
|
+
try {
|
|
1153
|
+
mockResponse = JSON.parse(mockResponseText);
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
showMcpError("模拟返回结果不是有效的 JSON 格式: " + e.message);
|
|
1156
|
+
return;
|
|
1193
1157
|
}
|
|
1194
1158
|
}
|
|
1195
1159
|
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
});
|
|
1160
|
+
// 构建 inputSchema
|
|
1161
|
+
const inputSchema = {
|
|
1162
|
+
type: "object",
|
|
1163
|
+
properties: {},
|
|
1164
|
+
required: [],
|
|
1165
|
+
};
|
|
1203
1166
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const packets = await this.queue.dequeue(6, timeout, (count) => {
|
|
1207
|
-
console.log(`缓冲超时,当前缓冲包数: ${count},开始播放`, "info");
|
|
1208
|
-
});
|
|
1209
|
-
if (packets.length) {
|
|
1210
|
-
console.log(`已缓冲 ${packets.length} 个音频包,开始播放`, "info");
|
|
1211
|
-
this.streamingContext.pushAudioBuffer(packets);
|
|
1212
|
-
}
|
|
1167
|
+
mcpProperties.forEach((prop) => {
|
|
1168
|
+
const propSchema = { type: prop.type };
|
|
1213
1169
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
if (data.length) {
|
|
1217
|
-
this.streamingContext.pushAudioBuffer(data);
|
|
1218
|
-
} else {
|
|
1219
|
-
break;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1170
|
+
if (prop.description) {
|
|
1171
|
+
propSchema.description = prop.description;
|
|
1222
1172
|
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// 播放已缓冲的音频
|
|
1226
|
-
async playBufferedAudio() {
|
|
1227
|
-
try {
|
|
1228
|
-
this.audioContext = this.getAudioContext();
|
|
1229
1173
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
this.opusDecoder = await this.initOpusDecoder();
|
|
1234
|
-
if (!this.opusDecoder) {
|
|
1235
|
-
throw new Error("解码器初始化失败");
|
|
1236
|
-
}
|
|
1237
|
-
console.log("Opus解码器初始化成功", "success");
|
|
1238
|
-
} catch (error) {
|
|
1239
|
-
console.log("Opus解码器初始化失败: " + error.message, "error");
|
|
1240
|
-
this.isPlaying = false;
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1174
|
+
if (prop.type === "integer" || prop.type === "number") {
|
|
1175
|
+
if (prop.minimum !== undefined && prop.minimum !== "") {
|
|
1176
|
+
propSchema.minimum = prop.minimum;
|
|
1243
1177
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
this.streamingContext = createStreamingContext(
|
|
1247
|
-
this.opusDecoder,
|
|
1248
|
-
this.audioContext,
|
|
1249
|
-
this.SAMPLE_RATE,
|
|
1250
|
-
this.CHANNELS,
|
|
1251
|
-
this.MIN_AUDIO_DURATION
|
|
1252
|
-
);
|
|
1178
|
+
if (prop.maximum !== undefined && prop.maximum !== "") {
|
|
1179
|
+
propSchema.maximum = prop.maximum;
|
|
1253
1180
|
}
|
|
1254
|
-
|
|
1255
|
-
this.streamingContext.decodeOpusFrames();
|
|
1256
|
-
this.streamingContext.startPlaying();
|
|
1257
|
-
} catch (error) {
|
|
1258
|
-
console.log(`播放已缓冲的音频出错: ${error.message}`, "error");
|
|
1259
|
-
this.isPlaying = false;
|
|
1260
|
-
this.streamingContext = null;
|
|
1261
1181
|
}
|
|
1262
|
-
}
|
|
1263
1182
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
} else {
|
|
1269
|
-
console.log("收到空音频数据帧,可能是结束标志", "warning");
|
|
1270
|
-
if (this.isPlaying && this.streamingContext) {
|
|
1271
|
-
this.streamingContext.endOfStream = true;
|
|
1272
|
-
}
|
|
1183
|
+
inputSchema.properties[prop.name] = propSchema;
|
|
1184
|
+
|
|
1185
|
+
if (prop.required) {
|
|
1186
|
+
inputSchema.required.push(prop.name);
|
|
1273
1187
|
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
if (inputSchema.required.length === 0) {
|
|
1191
|
+
delete inputSchema.required;
|
|
1274
1192
|
}
|
|
1275
1193
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
`Opus解码器预加载失败: ${error.message},将在需要时重试`,
|
|
1285
|
-
"warning"
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1194
|
+
const tool = { name, description, inputSchema, mockResponse };
|
|
1195
|
+
|
|
1196
|
+
if (mcpEditingIndex !== null) {
|
|
1197
|
+
mcpTools[mcpEditingIndex] = tool;
|
|
1198
|
+
console.log(`已更新工具: ${name}`, "success");
|
|
1199
|
+
} else {
|
|
1200
|
+
mcpTools.push(tool);
|
|
1201
|
+
console.log(`已添加工具: ${name}`, "success");
|
|
1288
1202
|
}
|
|
1289
1203
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1204
|
+
saveMcpTools();
|
|
1205
|
+
renderMcpTools();
|
|
1206
|
+
closeMcpModal();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* 显示错误
|
|
1211
|
+
*/
|
|
1212
|
+
function showMcpError(message) {
|
|
1213
|
+
const errorContainer = document.getElementById("mcpErrorContainer");
|
|
1214
|
+
errorContainer.innerHTML = `<div class="mcp-error">${message}</div>`;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* 编辑工具
|
|
1219
|
+
*/
|
|
1220
|
+
function editMcpTool(index) {
|
|
1221
|
+
openMcpModal(index);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* 删除工具
|
|
1226
|
+
*/
|
|
1227
|
+
function deleteMcpTool(index) {
|
|
1228
|
+
const isConnected = websocket && websocket.readyState === WebSocket.OPEN;
|
|
1229
|
+
if (isConnected) {
|
|
1230
|
+
alert("WebSocket 已连接,无法编辑工具");
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (confirm(`确定要删除工具 "${mcpTools[index].name}" 吗?`)) {
|
|
1234
|
+
const toolName = mcpTools[index].name;
|
|
1235
|
+
mcpTools.splice(index, 1);
|
|
1236
|
+
saveMcpTools();
|
|
1237
|
+
renderMcpTools();
|
|
1238
|
+
console.log(`已删除工具: ${toolName}`, "info");
|
|
1295
1239
|
}
|
|
1296
1240
|
}
|
|
1297
1241
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* 保存工具
|
|
1244
|
+
*/
|
|
1245
|
+
function saveMcpTools() {
|
|
1246
|
+
localStorage.setItem("mcpTools", JSON.stringify(mcpTools));
|
|
1247
|
+
}
|
|
1300
1248
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1249
|
+
/**
|
|
1250
|
+
* 获取工具列表
|
|
1251
|
+
*/
|
|
1252
|
+
function getMcpTools() {
|
|
1253
|
+
return mcpTools.map((tool) => ({
|
|
1254
|
+
name: tool.name,
|
|
1255
|
+
description: tool.description,
|
|
1256
|
+
inputSchema: tool.inputSchema,
|
|
1257
|
+
}));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* 执行工具调用
|
|
1262
|
+
*/
|
|
1263
|
+
function executeMcpTool(toolName, toolArgs) {
|
|
1264
|
+
const tool = mcpTools.find((t) => t.name === toolName);
|
|
1265
|
+
if (!tool) {
|
|
1266
|
+
console.log(`未找到工具: ${toolName}`, "error");
|
|
1267
|
+
return {
|
|
1268
|
+
success: false,
|
|
1269
|
+
error: `未知工具: ${toolName}`,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (tool.name == "self.drink_car_list") {
|
|
1274
|
+
console.log("准备触发 gotoOrderEvent 事件"); // 增加此日志
|
|
1275
|
+
const event = new CustomEvent("gotoOrderEvent");
|
|
1276
|
+
window.dispatchEvent(event);
|
|
1277
|
+
return {
|
|
1278
|
+
success: true,
|
|
1279
|
+
message: `工具 ${toolName} 执行成功`,
|
|
1280
|
+
data: sessionStorage.getItem('cartList') || [],
|
|
1281
|
+
};
|
|
1282
|
+
} else if (tool.name == "self.drink_car_reset") {
|
|
1283
|
+
console.log("准备触发 resetOrderEvent 事件"); // 增加此日志
|
|
1284
|
+
const event = new CustomEvent("resetOrderEvent", {
|
|
1285
|
+
detail: toolArgs,
|
|
1286
|
+
});
|
|
1287
|
+
window.dispatchEvent(event);
|
|
1288
|
+
return {
|
|
1289
|
+
success: true,
|
|
1290
|
+
message: `工具 ${toolName} 执行成功`,
|
|
1291
|
+
data: sessionStorage.getItem('cartList') || [],
|
|
1292
|
+
}
|
|
1293
|
+
} else if (tool.name == "self.drink_order") {
|
|
1294
|
+
console.log("准备触发 orderEvent 事件"); // 增加此日志
|
|
1295
|
+
const event = new CustomEvent("orderEvent");
|
|
1296
|
+
window.dispatchEvent(event);
|
|
1304
1297
|
}
|
|
1305
|
-
return audioPlayerInstance;
|
|
1306
1298
|
}
|
|
1307
1299
|
|
|
1300
|
+
// 暴露全局方法供 HTML 内联事件调用
|
|
1301
|
+
window.mcpModule = {
|
|
1302
|
+
updateMcpProperty,
|
|
1303
|
+
deleteMcpProperty,
|
|
1304
|
+
editMcpTool,
|
|
1305
|
+
deleteMcpTool,
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
1308
|
;// CONCATENATED MODULE: ./src/utils/recorder.js
|
|
1309
1309
|
// 音频录制模块
|
|
1310
1310
|
|
|
@@ -1340,7 +1340,7 @@ class AudioRecorder {
|
|
|
1340
1340
|
|
|
1341
1341
|
// 获取AudioContext实例
|
|
1342
1342
|
getAudioContext() {
|
|
1343
|
-
const audioPlayer =
|
|
1343
|
+
const audioPlayer = getAudioPlayer();
|
|
1344
1344
|
return audioPlayer.getAudioContext();
|
|
1345
1345
|
}
|
|
1346
1346
|
|
|
@@ -2150,7 +2150,7 @@ class WebSocketHandler {
|
|
|
2150
2150
|
}
|
|
2151
2151
|
|
|
2152
2152
|
const opusData = new Uint8Array(arrayBuffer);
|
|
2153
|
-
const audioPlayer =
|
|
2153
|
+
const audioPlayer = getAudioPlayer();
|
|
2154
2154
|
audioPlayer.enqueueAudioData(opusData);
|
|
2155
2155
|
} catch (error) {
|
|
2156
2156
|
console.log(`处理二进制消息出错: ${error.message}`, "error");
|
|
@@ -2487,6 +2487,7 @@ function isWsConnected() {
|
|
|
2487
2487
|
|
|
2488
2488
|
|
|
2489
2489
|
|
|
2490
|
+
|
|
2490
2491
|
/* harmony default export */ const lib_vue_loader_options_srcvue_type_script_lang_js = ({
|
|
2491
2492
|
name: "ibiAiTalk",
|
|
2492
2493
|
data() {
|