myagent-ai 1.7.1 → 1.7.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/package.json +2 -2
- package/web/ui/chat/chat_main.js +254 -25
- package/web/ui/chat/flow_engine.js +9 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagent-ai",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
|
|
5
5
|
"main": "main.py",
|
|
6
6
|
"bin": {
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"departments/",
|
|
66
66
|
"web/"
|
|
67
67
|
]
|
|
68
|
-
}
|
|
68
|
+
}
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2582,6 +2582,7 @@ function insertQuick(text) {
|
|
|
2582
2582
|
// ══════════════════════════════════════════════════════
|
|
2583
2583
|
// ── TTS (Text-to-Speech) Manager ──
|
|
2584
2584
|
// ══════════════════════════════════════════════════════
|
|
2585
|
+
// 支持分段流式播放:文本边生成边朗读,遇到句子边界立即合成播放
|
|
2585
2586
|
|
|
2586
2587
|
// Simple hash function for text caching
|
|
2587
2588
|
function simpleHash(str) {
|
|
@@ -2603,6 +2604,13 @@ const ttsManager = {
|
|
|
2603
2604
|
cache: new Map(), // textHash -> blobUrl
|
|
2604
2605
|
voice: 'zh-CN-XiaoxiaoNeural',
|
|
2605
2606
|
speed: '+0%',
|
|
2607
|
+
// ── 分段流式状态 ──
|
|
2608
|
+
_streamActive: false, // 是否正在流式模式
|
|
2609
|
+
_streamBuffer: '', // 当前缓冲区(积累到句子边界前)
|
|
2610
|
+
_audioQueue: [], // 待播放的音频 blobUrl 队列
|
|
2611
|
+
_audioPlaying: false, // 队列是否正在播放
|
|
2612
|
+
_stopRequested: false, // 是否已请求停止
|
|
2613
|
+
_streamMsgIndex: -1, // 流式模式对应的消息索引
|
|
2606
2614
|
|
|
2607
2615
|
init() {
|
|
2608
2616
|
// Load TTS enabled state from localStorage
|
|
@@ -2613,15 +2621,25 @@ const ttsManager = {
|
|
|
2613
2621
|
this.updateButtonUI();
|
|
2614
2622
|
// Audio event handlers
|
|
2615
2623
|
this.audio.addEventListener('ended', () => {
|
|
2616
|
-
this.
|
|
2617
|
-
|
|
2618
|
-
|
|
2624
|
+
if (this._streamActive) {
|
|
2625
|
+
// 流式模式:播放队列下一段
|
|
2626
|
+
this._playNextInQueue();
|
|
2627
|
+
} else {
|
|
2628
|
+
this.isPlaying = false;
|
|
2629
|
+
this.currentMsgIndex = -1;
|
|
2630
|
+
this.updatePlayingIndicator();
|
|
2631
|
+
}
|
|
2619
2632
|
});
|
|
2620
2633
|
this.audio.addEventListener('error', (e) => {
|
|
2621
2634
|
console.error('TTS audio error:', e);
|
|
2622
|
-
this.
|
|
2623
|
-
|
|
2624
|
-
|
|
2635
|
+
if (this._streamActive) {
|
|
2636
|
+
// 流式模式:跳过错误段,播放下一段
|
|
2637
|
+
this._playNextInQueue();
|
|
2638
|
+
} else {
|
|
2639
|
+
this.isPlaying = false;
|
|
2640
|
+
this.currentMsgIndex = -1;
|
|
2641
|
+
this.updatePlayingIndicator();
|
|
2642
|
+
}
|
|
2625
2643
|
});
|
|
2626
2644
|
},
|
|
2627
2645
|
|
|
@@ -2652,10 +2670,16 @@ const ttsManager = {
|
|
|
2652
2670
|
},
|
|
2653
2671
|
|
|
2654
2672
|
stop() {
|
|
2673
|
+
this._stopRequested = true;
|
|
2655
2674
|
this.audio.pause();
|
|
2656
2675
|
this.audio.currentTime = 0;
|
|
2657
2676
|
this.isPlaying = false;
|
|
2658
2677
|
this.currentMsgIndex = -1;
|
|
2678
|
+
this._streamActive = false;
|
|
2679
|
+
this._streamBuffer = '';
|
|
2680
|
+
this._audioQueue = [];
|
|
2681
|
+
this._audioPlaying = false;
|
|
2682
|
+
this._streamMsgIndex = -1;
|
|
2659
2683
|
this.updatePlayingIndicator();
|
|
2660
2684
|
},
|
|
2661
2685
|
|
|
@@ -2666,10 +2690,216 @@ const ttsManager = {
|
|
|
2666
2690
|
}
|
|
2667
2691
|
},
|
|
2668
2692
|
|
|
2693
|
+
// ════════════════════════════════════════════
|
|
2694
|
+
// ── 分段流式 TTS:text_delta 回调 ──
|
|
2695
|
+
// ════════════════════════════════════════════
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* 开始流式 TTS 会话
|
|
2699
|
+
* @param {number} msgIndex - 消息索引
|
|
2700
|
+
*/
|
|
2701
|
+
_startStream(msgIndex) {
|
|
2702
|
+
this._stopRequested = false;
|
|
2703
|
+
this._streamActive = true;
|
|
2704
|
+
this._streamBuffer = '';
|
|
2705
|
+
this._audioQueue = [];
|
|
2706
|
+
this._audioPlaying = false;
|
|
2707
|
+
this._streamMsgIndex = msgIndex;
|
|
2708
|
+
this.currentMsgIndex = msgIndex;
|
|
2709
|
+
this.isPlaying = true;
|
|
2710
|
+
},
|
|
2711
|
+
|
|
2712
|
+
/**
|
|
2713
|
+
* 流式推送文本增量
|
|
2714
|
+
* 在 flow_engine.js 的 text_delta 处理中调用
|
|
2715
|
+
* 积累到句子边界时自动触发 TTS 合成
|
|
2716
|
+
* @param {string} delta - 新增文本片段
|
|
2717
|
+
*/
|
|
2718
|
+
streamDelta(delta) {
|
|
2719
|
+
if (!this.enabled || !this._streamActive || this._stopRequested) return;
|
|
2720
|
+
if (!delta || !delta.trim()) return;
|
|
2721
|
+
|
|
2722
|
+
this._streamBuffer += delta;
|
|
2723
|
+
|
|
2724
|
+
// 检测句子边界:中文句号/感叹号/问号,英文句号+空格,或换行
|
|
2725
|
+
var boundaryPattern = /[。!?]|\.(?:\s|$)|\n/;
|
|
2726
|
+
var boundaryIdx = -1;
|
|
2727
|
+
for (var i = 0; i < this._streamBuffer.length; i++) {
|
|
2728
|
+
if (boundaryPattern.test(this._streamBuffer[i])) {
|
|
2729
|
+
boundaryIdx = i;
|
|
2730
|
+
break;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// 还没到句子边界,但如果缓冲区已经很长(>200字),强制切分
|
|
2735
|
+
if (boundaryIdx === -1 && this._streamBuffer.length > 200) {
|
|
2736
|
+
// 在最后一个逗号或空格处切分
|
|
2737
|
+
var lastComma = -1;
|
|
2738
|
+
for (var j = 0; j < this._streamBuffer.length; j++) {
|
|
2739
|
+
var ch = this._streamBuffer[j];
|
|
2740
|
+
if (ch === ',' || ch === ',' || ch === ';' || ch === ';' || ch === ' ' || ch === ':') {
|
|
2741
|
+
lastComma = j;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
if (lastComma > 0) {
|
|
2745
|
+
boundaryIdx = lastComma;
|
|
2746
|
+
} else {
|
|
2747
|
+
boundaryIdx = this._streamBuffer.length;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
if (boundaryIdx !== -1) {
|
|
2752
|
+
// 提取到边界的文本
|
|
2753
|
+
var sentence = this._streamBuffer.substring(0, boundaryIdx + 1).trim();
|
|
2754
|
+
this._streamBuffer = this._streamBuffer.substring(boundaryIdx + 1);
|
|
2755
|
+
|
|
2756
|
+
if (sentence) {
|
|
2757
|
+
var cleanSentence = this._cleanForStreamTTS(sentence);
|
|
2758
|
+
if (cleanSentence) {
|
|
2759
|
+
this._enqueueTTS(cleanSentence);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
},
|
|
2764
|
+
|
|
2765
|
+
/**
|
|
2766
|
+
* 刷新剩余缓冲区(流结束时调用)
|
|
2767
|
+
* 将 buffer 中剩余的文本立即合成
|
|
2768
|
+
*/
|
|
2769
|
+
streamFlush() {
|
|
2770
|
+
if (!this.enabled || !this._streamActive || this._stopRequested) return;
|
|
2771
|
+
var remaining = this._streamBuffer.trim();
|
|
2772
|
+
this._streamBuffer = '';
|
|
2773
|
+
if (remaining) {
|
|
2774
|
+
var cleanText = this._cleanForStreamTTS(remaining);
|
|
2775
|
+
if (cleanText) {
|
|
2776
|
+
this._enqueueTTS(cleanText);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
// 标记流式阶段结束(队列播完后自动清理状态)
|
|
2780
|
+
this._streamActive = false;
|
|
2781
|
+
},
|
|
2782
|
+
|
|
2783
|
+
/**
|
|
2784
|
+
* 清理文本用于流式 TTS(去 HTML/代码块/执行结果等)
|
|
2785
|
+
*/
|
|
2786
|
+
_cleanForStreamTTS(text) {
|
|
2787
|
+
// 去除代码块
|
|
2788
|
+
text = text.replace(/```[\s\S]*?```/g, '');
|
|
2789
|
+
// 去除执行结果标记
|
|
2790
|
+
text = text.replace(/^\s*[✅❌⏰]\s*\[执行结果\].*/gm, '');
|
|
2791
|
+
// 去除 HTML 标签
|
|
2792
|
+
text = text.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '');
|
|
2793
|
+
text = text.replace(/<img[^>]*>/gi, '');
|
|
2794
|
+
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
2795
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
2796
|
+
// 去除 emoji
|
|
2797
|
+
text = text.replace(/[\u{1F300}-\u{1FAFF}]/gu, '');
|
|
2798
|
+
text = text.replace(/[\u{2600}-\u{27BF}]/gu, '');
|
|
2799
|
+
text = text.replace(/[\u{FE00}-\u{FE0F}]/gu, '');
|
|
2800
|
+
text = text.replace(/[\u{200D}]/gu, '');
|
|
2801
|
+
text = text.replace(/[\u{20E3}]/gu, '');
|
|
2802
|
+
text = text.replace(/[\u{2300}-\u{23FF}]/gu, '');
|
|
2803
|
+
text = text.replace(/[\u{2B50}-\u{2B55}]/gu, '');
|
|
2804
|
+
text = text.replace(/[\u{203C}-\u{3299}]/gu, '');
|
|
2805
|
+
text = text.replace(/[\u{E0020}-\u{E007F}]/gu, '');
|
|
2806
|
+
text = text.replace(/[✅❌⚠️🔄⏰🔒💻🔍📁🧠🌐🛠👋🤖🎯💡🚀👍🎯📊📝🔊🔍💬📌✨✓✗→←↓↑⏹⬇⬆↩]/g, '');
|
|
2807
|
+
// 去除多余换行
|
|
2808
|
+
text = text.replace(/\n{2,}/g, '\n');
|
|
2809
|
+
text = text.trim();
|
|
2810
|
+
return text || null;
|
|
2811
|
+
},
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* 将文本加入 TTS 合成队列(异步,不阻塞)
|
|
2815
|
+
*/
|
|
2816
|
+
_enqueueTTS(text) {
|
|
2817
|
+
if (this._stopRequested) return;
|
|
2818
|
+
var self = this;
|
|
2819
|
+
|
|
2820
|
+
(async function() {
|
|
2821
|
+
try {
|
|
2822
|
+
var hash = simpleHash(text);
|
|
2823
|
+
var blobUrl = self.cache.get(hash);
|
|
2824
|
+
|
|
2825
|
+
if (!blobUrl) {
|
|
2826
|
+
var resp = await fetch('/api/tts', {
|
|
2827
|
+
method: 'POST',
|
|
2828
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2829
|
+
body: JSON.stringify({
|
|
2830
|
+
text: text,
|
|
2831
|
+
voice: self.voice,
|
|
2832
|
+
speed: self.speed,
|
|
2833
|
+
}),
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
if (!resp.ok) {
|
|
2837
|
+
var errData = await resp.json().catch(function() { return {}; });
|
|
2838
|
+
throw new Error(errData.error || 'TTS 请求失败');
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
var blob = await resp.blob();
|
|
2842
|
+
blobUrl = URL.createObjectURL(blob);
|
|
2843
|
+
self.cache.set(hash, blobUrl);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
if (!self._stopRequested) {
|
|
2847
|
+
self._audioQueue.push(blobUrl);
|
|
2848
|
+
// 如果还没开始播放队列,立即开始
|
|
2849
|
+
if (!self._audioPlaying) {
|
|
2850
|
+
self._playNextInQueue();
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
} catch (e) {
|
|
2854
|
+
console.error('TTS stream chunk error:', e);
|
|
2855
|
+
}
|
|
2856
|
+
})();
|
|
2857
|
+
},
|
|
2858
|
+
|
|
2859
|
+
/**
|
|
2860
|
+
* 播放队列中的下一段音频
|
|
2861
|
+
*/
|
|
2862
|
+
_playNextInQueue() {
|
|
2863
|
+
if (this._stopRequested) {
|
|
2864
|
+
this.isPlaying = false;
|
|
2865
|
+
this._audioPlaying = false;
|
|
2866
|
+
this.currentMsgIndex = -1;
|
|
2867
|
+
this.updatePlayingIndicator();
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
if (this._audioQueue.length === 0) {
|
|
2872
|
+
// 队列空了,检查流式是否已结束
|
|
2873
|
+
if (!this._streamActive) {
|
|
2874
|
+
// 流结束且队列为空 → 播放完成
|
|
2875
|
+
this.isPlaying = false;
|
|
2876
|
+
this._audioPlaying = false;
|
|
2877
|
+
this.currentMsgIndex = -1;
|
|
2878
|
+
this.updatePlayingIndicator();
|
|
2879
|
+
}
|
|
2880
|
+
// 如果流还在继续,等待新的音频入队
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
var blobUrl = this._audioQueue.shift();
|
|
2885
|
+
this.audio.src = blobUrl;
|
|
2886
|
+
this._audioPlaying = true;
|
|
2887
|
+
|
|
2888
|
+
var self = this;
|
|
2889
|
+
this.audio.play().catch(function(e) {
|
|
2890
|
+
console.error('TTS play queue error:', e);
|
|
2891
|
+
self._playNextInQueue();
|
|
2892
|
+
});
|
|
2893
|
+
},
|
|
2894
|
+
|
|
2895
|
+
// ════════════════════════════════════════════
|
|
2896
|
+
// ── 完整消息 TTS(非流式,兼容手动点击) ──
|
|
2897
|
+
// ════════════════════════════════════════════
|
|
2898
|
+
|
|
2669
2899
|
async speak(msgIndex) {
|
|
2670
2900
|
if (msgIndex < 0 || msgIndex >= state.messages.length) return;
|
|
2671
2901
|
const msg = state.messages[msgIndex];
|
|
2672
|
-
if (!msg || msg.role !== '
|
|
2902
|
+
if (!msg || msg.role !== 'assistant' && !msg.content) return;
|
|
2673
2903
|
|
|
2674
2904
|
// 跳过命令执行结果(以 [执行结果] 开头的消息)
|
|
2675
2905
|
var rawText = msg.content.replace(/<[^>]+>/g, '');
|
|
@@ -2677,24 +2907,23 @@ const ttsManager = {
|
|
|
2677
2907
|
|
|
2678
2908
|
// 去除 HTML 标签(msg.content 是 HTML 格式,SVG 图标等会被朗读)
|
|
2679
2909
|
let text = msg.content
|
|
2680
|
-
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
2681
|
-
.replace(/<img[^>]*>/gi, '[图片]')
|
|
2682
|
-
.replace(/<br\s*\/?>/gi, '\n')
|
|
2683
|
-
.replace(/<[^>]+>/g, '')
|
|
2684
|
-
|
|
2685
|
-
.replace(/[\u{
|
|
2686
|
-
.replace(/[\u{
|
|
2687
|
-
.replace(/[\u{
|
|
2688
|
-
.replace(/[\u{
|
|
2689
|
-
.replace(/[\u{
|
|
2690
|
-
.replace(/[\u{
|
|
2691
|
-
.replace(/[\u{
|
|
2692
|
-
.replace(/[\u{
|
|
2693
|
-
.replace(/[
|
|
2694
|
-
.replace(
|
|
2695
|
-
.replace(
|
|
2696
|
-
.replace(
|
|
2697
|
-
.replace(/\n{2,}/g, '\n') // 多余换行
|
|
2910
|
+
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
2911
|
+
.replace(/<img[^>]*>/gi, '[图片]')
|
|
2912
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
2913
|
+
.replace(/<[^>]+>/g, '')
|
|
2914
|
+
.replace(/[\u{1F300}-\u{1FAFF}]/gu, '')
|
|
2915
|
+
.replace(/[\u{2600}-\u{27BF}]/gu, '')
|
|
2916
|
+
.replace(/[\u{FE00}-\u{FE0F}]/gu, '')
|
|
2917
|
+
.replace(/[\u{200D}]/gu, '')
|
|
2918
|
+
.replace(/[\u{20E3}]/gu, '')
|
|
2919
|
+
.replace(/[\u{2300}-\u{23FF}]/gu, '')
|
|
2920
|
+
.replace(/[\u{2B50}-\u{2B55}]/gu, '')
|
|
2921
|
+
.replace(/[\u{203C}-\u{3299}]/gu, '')
|
|
2922
|
+
.replace(/[\u{E0020}-\u{E007F}]/gu, '')
|
|
2923
|
+
.replace(/[✅❌⚠️🔄⏰🔒💻🔍📁🧠🌐🛠👋🤖🎯💡🚀👍🎯📊📝🔊🔍💬📌✨✓✗→←↓↑⏹⬇⬆↩]/g, '')
|
|
2924
|
+
.replace(/```[\s\S]*?```/g, '代码块')
|
|
2925
|
+
.replace(/`[^`]+`/g, function(m) { return m.slice(1,-1); })
|
|
2926
|
+
.replace(/\n{2,}/g, '\n')
|
|
2698
2927
|
.trim();
|
|
2699
2928
|
|
|
2700
2929
|
if (!text) return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<string>:27: SyntaxWarning: invalid escape sequence '\s'
|
|
1
2
|
// ══════════════════════════════════════════════════════
|
|
2
3
|
// ── Flow Engine: 文本处理流引擎 ──
|
|
3
4
|
// ── 负责消息发送、SSE 流式处理、大文本检测与分段、
|
|
@@ -738,6 +739,11 @@ async function sendMessage() {
|
|
|
738
739
|
fullResponse += evt.content;
|
|
739
740
|
state.messages[msgIdx].content = fullResponse;
|
|
740
741
|
throttledStreamUpdate(msgIdx);
|
|
742
|
+
// ── 分段流式 TTS:推送增量文本 ──
|
|
743
|
+
if (ttsManager.enabled && !ttsManager._streamActive) {
|
|
744
|
+
ttsManager._startStream(msgIdx);
|
|
745
|
+
}
|
|
746
|
+
ttsManager.streamDelta(evt.content);
|
|
741
747
|
} else if (evt.type === 'thought_delta') {
|
|
742
748
|
// Agent 思考过程增量文本(流式推送,单独显示)
|
|
743
749
|
fullThought += evt.content;
|
|
@@ -843,10 +849,9 @@ async function sendMessage() {
|
|
|
843
849
|
state.agentSessions[state.activeAgent] = [...state.sessions];
|
|
844
850
|
renderSessions();
|
|
845
851
|
|
|
846
|
-
//
|
|
847
|
-
if (ttsManager.enabled &&
|
|
848
|
-
|
|
849
|
-
ttsManager.speak(idx);
|
|
852
|
+
// ── 分段流式 TTS:刷新剩余缓冲区 ──
|
|
853
|
+
if (ttsManager.enabled && ttsManager._streamActive) {
|
|
854
|
+
ttsManager.streamFlush();
|
|
850
855
|
}
|
|
851
856
|
} catch (e) {
|
|
852
857
|
if (e.name === 'AbortError') {
|