myagent-ai 1.7.0 → 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/api_server.py +102 -10
- 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/api_server.py
CHANGED
|
@@ -2611,10 +2611,29 @@ class ApiServer:
|
|
|
2611
2611
|
max_iter = agent.config.agent.max_iterations if agent.config else 30
|
|
2612
2612
|
final_response = ""
|
|
2613
2613
|
iteration = 0
|
|
2614
|
+
# 追踪连续无 action 迭代次数,防止无限重新提示
|
|
2615
|
+
_consecutive_no_action = 0
|
|
2616
|
+
_MAX_NO_ACTION_RETRIES = 3
|
|
2614
2617
|
|
|
2615
2618
|
while iteration < max_iter:
|
|
2616
2619
|
iteration += 1
|
|
2617
2620
|
|
|
2621
|
+
# ── 执行模式:每轮迭代前刷新任务进度上下文 ──
|
|
2622
|
+
# 这样 LLM 在每次调用时都能看到最新的任务列表状态
|
|
2623
|
+
if chat_mode == "exec":
|
|
2624
|
+
fresh_task_context = self._build_task_plan_context(agent_path, chat_mode, user_message)
|
|
2625
|
+
if fresh_task_context:
|
|
2626
|
+
# 替换掉之前的任务规划上下文(保留非任务规划部分)
|
|
2627
|
+
base_prompt = context.metadata.get("agent_override_prompt", "")
|
|
2628
|
+
task_section_marker = "\n\n## 任务规划\n"
|
|
2629
|
+
idx = base_prompt.find(task_section_marker)
|
|
2630
|
+
if idx >= 0:
|
|
2631
|
+
# 替换旧的任务规划上下文
|
|
2632
|
+
context.metadata["agent_override_prompt"] = base_prompt[:idx] + task_section_marker + fresh_task_context
|
|
2633
|
+
else:
|
|
2634
|
+
# 没有旧的任务规划上下文,追加
|
|
2635
|
+
context.metadata["agent_override_prompt"] = base_prompt + task_section_marker + fresh_task_context
|
|
2636
|
+
|
|
2618
2637
|
# Build messages
|
|
2619
2638
|
messages = agent._build_messages(context)
|
|
2620
2639
|
tools = agent._get_tools()
|
|
@@ -2634,7 +2653,9 @@ class ApiServer:
|
|
|
2634
2653
|
}
|
|
2635
2654
|
|
|
2636
2655
|
# 需要回退(hold back)的最大字符数,用于检测 ```action 或 ```tasklist 标记
|
|
2637
|
-
|
|
2656
|
+
# 注意: 12 足够覆盖 ```tasklist\n (12字符) + 余量
|
|
2657
|
+
# 过大的值会导致短文本被 hold 住,流结束后一次性输出(看起来像非流式)
|
|
2658
|
+
_MAX_HOLD = 12
|
|
2638
2659
|
|
|
2639
2660
|
async def _text_delta_callback(full_text_so_far: str, delta_text: str):
|
|
2640
2661
|
"""智能流式过滤器:文本正常推送,JSON action 块拦截"""
|
|
@@ -2661,7 +2682,8 @@ class ApiServer:
|
|
|
2661
2682
|
text_before = remaining[:marker_pos]
|
|
2662
2683
|
if text_before.strip():
|
|
2663
2684
|
await _write_sse({"type": "text_delta", "content": text_before})
|
|
2664
|
-
|
|
2685
|
+
# 跳过整个开始标记(```action 或 ```tasklist),不要只跳到 ```
|
|
2686
|
+
st["processed_pos"] += marker_pos + len(f"```{block_type}")
|
|
2665
2687
|
if block_type == "tasklist":
|
|
2666
2688
|
st["mode"] = "tasklist_block"
|
|
2667
2689
|
else:
|
|
@@ -2706,10 +2728,14 @@ class ApiServer:
|
|
|
2706
2728
|
|
|
2707
2729
|
elif st["mode"] == "action_block":
|
|
2708
2730
|
# ── Action 代码块模式:寻找结束标记 ``` ──
|
|
2709
|
-
|
|
2731
|
+
# 结束标记是独立的 ``` 行(前面有换行),不要匹配块开始标记内的 ```
|
|
2732
|
+
end_marker = remaining.find("\n```")
|
|
2733
|
+
if end_marker < 0 and remaining.startswith("```"):
|
|
2734
|
+
end_marker = 0
|
|
2710
2735
|
if end_marker >= 0:
|
|
2711
2736
|
# 找到结束标记,跳过整个 action 块(不推送)
|
|
2712
|
-
|
|
2737
|
+
skip_len = (end_marker + 1 + 3) if end_marker > 0 else 3
|
|
2738
|
+
st["processed_pos"] += skip_len
|
|
2713
2739
|
st["mode"] = "text"
|
|
2714
2740
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
2715
2741
|
continue
|
|
@@ -2719,10 +2745,13 @@ class ApiServer:
|
|
|
2719
2745
|
|
|
2720
2746
|
elif st["mode"] == "tasklist_block":
|
|
2721
2747
|
# ── Tasklist 代码块模式:寻找结束标记 ``` ──
|
|
2722
|
-
end_marker = remaining.find("```")
|
|
2748
|
+
end_marker = remaining.find("\n```")
|
|
2749
|
+
if end_marker < 0 and remaining.startswith("```"):
|
|
2750
|
+
end_marker = 0
|
|
2723
2751
|
if end_marker >= 0:
|
|
2724
2752
|
# 找到结束标记,跳过整个 tasklist 块(不推送)
|
|
2725
|
-
|
|
2753
|
+
skip_len = (end_marker + 1 + 3) if end_marker > 0 else 3
|
|
2754
|
+
st["processed_pos"] += skip_len
|
|
2726
2755
|
st["mode"] = "text"
|
|
2727
2756
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
2728
2757
|
continue
|
|
@@ -2756,7 +2785,11 @@ class ApiServer:
|
|
|
2756
2785
|
st = _stream_state
|
|
2757
2786
|
remaining = full_text[st["processed_pos"]:]
|
|
2758
2787
|
if remaining.strip() and st["mode"] == "text":
|
|
2759
|
-
|
|
2788
|
+
# 如果剩余文本较长,逐 token 推送以保持流式体验
|
|
2789
|
+
if len(remaining) > 20:
|
|
2790
|
+
await _stream_text_chunked(remaining, _write_sse, chunk_size=3, delay=0.01)
|
|
2791
|
+
else:
|
|
2792
|
+
await _write_sse({"type": "text_delta", "content": remaining})
|
|
2760
2793
|
st["processed_pos"] = len(full_text)
|
|
2761
2794
|
|
|
2762
2795
|
# Call LLM with streaming — tokens are filtered through _text_delta_callback
|
|
@@ -2825,6 +2858,8 @@ class ApiServer:
|
|
|
2825
2858
|
)
|
|
2826
2859
|
continue # Next iteration — let LLM process tool results
|
|
2827
2860
|
|
|
2861
|
+
# 注意:成功处理 tool_calls 或 actions 后,_consecutive_no_action 会被重置
|
|
2862
|
+
|
|
2828
2863
|
# ── 从混合内容中提取 JSON action 指令 ──
|
|
2829
2864
|
# 支持格式:
|
|
2830
2865
|
# 1. 纯 JSON(向后兼容)
|
|
@@ -2945,6 +2980,7 @@ class ApiServer:
|
|
|
2945
2980
|
|
|
2946
2981
|
# Continue the loop — let LLM decide next step based on execution results
|
|
2947
2982
|
# tasklist 已在循环顶部 LLM 调用后提取并推送,此处无需重复
|
|
2983
|
+
_consecutive_no_action = 0 # 成功执行 action,重置无 action 计数
|
|
2948
2984
|
|
|
2949
2985
|
continue
|
|
2950
2986
|
|
|
@@ -2956,10 +2992,66 @@ class ApiServer:
|
|
|
2956
2992
|
await _stream_text_chunked(final_response, _write_sse)
|
|
2957
2993
|
break
|
|
2958
2994
|
|
|
2959
|
-
# ── Pure text response (no actions/tool calls)
|
|
2995
|
+
# ── Pure text response (no actions/tool calls) ──
|
|
2960
2996
|
# Content was already streamed token-by-token via _text_delta_callback
|
|
2961
|
-
|
|
2962
|
-
|
|
2997
|
+
|
|
2998
|
+
# ── 执行模式多轮续命机制 ──
|
|
2999
|
+
# 问题:LLM 有时只输出文字总结而不输出 ```action``` 块,
|
|
3000
|
+
# 导致循环在此处 break,任务未完成就停止。
|
|
3001
|
+
# 修复:检查任务列表中是否还有未完成步骤,如果有则重新提示 LLM 继续。
|
|
3002
|
+
if chat_mode == "exec":
|
|
3003
|
+
current_tasks = self._task_list_store.get(agent_path, [])
|
|
3004
|
+
pending_count = sum(
|
|
3005
|
+
1 for t in current_tasks
|
|
3006
|
+
if t.get("status") in ("pending", "running", "blocked")
|
|
3007
|
+
)
|
|
3008
|
+
if pending_count > 0 and _consecutive_no_action < _MAX_NO_ACTION_RETRIES and iteration < max_iter - 1:
|
|
3009
|
+
# 还有未完成任务,LLM 忘记了输出 action 块,重新提示
|
|
3010
|
+
_consecutive_no_action += 1
|
|
3011
|
+
logger.info(
|
|
3012
|
+
f"[{session_id}] Exec 模式: LLM 未输出 action,"
|
|
3013
|
+
f"重新提示继续执行 ({_consecutive_no_action}/{_MAX_NO_ACTION_RETRIES})"
|
|
3014
|
+
)
|
|
3015
|
+
context.conversation_history.append(
|
|
3016
|
+
Message(role="assistant", content=content)
|
|
3017
|
+
)
|
|
3018
|
+
context.conversation_history.append(
|
|
3019
|
+
Message(
|
|
3020
|
+
role="user",
|
|
3021
|
+
content=(
|
|
3022
|
+
f"[系统提示] 任务尚未完成,仍有 {pending_count} 个未执行步骤。"
|
|
3023
|
+
"你必须继续执行操作:用 ```action``` 代码块输出操作指令。"
|
|
3024
|
+
"不要只输出文字总结或反馈,必须输出具体的操作命令。"
|
|
3025
|
+
),
|
|
3026
|
+
)
|
|
3027
|
+
)
|
|
3028
|
+
await _write_sse({
|
|
3029
|
+
"type": "thought",
|
|
3030
|
+
"content": (
|
|
3031
|
+
f"\n\n⏳ 检测到 {pending_count} 个未完成任务,"
|
|
3032
|
+
f"继续执行下一步操作..."
|
|
3033
|
+
),
|
|
3034
|
+
})
|
|
3035
|
+
# 清除上一轮的流式输出,为下一轮腾出空间
|
|
3036
|
+
await _write_sse({"type": "clear_text"})
|
|
3037
|
+
continue
|
|
3038
|
+
else:
|
|
3039
|
+
# 所有任务完成,或已达到重试上限
|
|
3040
|
+
if pending_count > 0 and _consecutive_no_action >= _MAX_NO_ACTION_RETRIES:
|
|
3041
|
+
logger.warning(
|
|
3042
|
+
f"[{session_id}] Exec 模式: 连续 {_MAX_NO_ACTION_RETRIES} 次"
|
|
3043
|
+
f"无 action 输出,强制结束循环"
|
|
3044
|
+
)
|
|
3045
|
+
# 在回复末尾追加提示
|
|
3046
|
+
final_response = content + (
|
|
3047
|
+
f"\n\n⚠️ 注意:仍有 {pending_count} 个任务未完成,"
|
|
3048
|
+
"但 Agent 已达到最大重试次数。你可以要求我继续完成这些任务。"
|
|
3049
|
+
)
|
|
3050
|
+
break
|
|
3051
|
+
else:
|
|
3052
|
+
# 非 exec 模式或无待办任务,正常结束
|
|
3053
|
+
final_response = content
|
|
3054
|
+
break
|
|
2963
3055
|
|
|
2964
3056
|
# Save assistant response to memory
|
|
2965
3057
|
if agent.memory and final_response:
|
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') {
|