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