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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.7.0",
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
- _MAX_HOLD = 22 # ```tasklist\n 最长 14 字符,留余量
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
- st["processed_pos"] += marker_pos
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
- end_marker = remaining.find("```")
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
- st["processed_pos"] += end_marker + 3 # 跳过 ```
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
- st["processed_pos"] += end_marker + 3 # 跳过 ```
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
- await _write_sse({"type": "text_delta", "content": remaining})
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) — this is the final answer ──
2995
+ # ── Pure text response (no actions/tool calls) ──
2960
2996
  # Content was already streamed token-by-token via _text_delta_callback
2961
- final_response = content
2962
- break
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:
@@ -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.isPlaying = false;
2617
- this.currentMsgIndex = -1;
2618
- this.updatePlayingIndicator();
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.isPlaying = false;
2623
- this.currentMsgIndex = -1;
2624
- this.updatePlayingIndicator();
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 !== 'user' && !msg.content) return;
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, '') // 移除 SVG 图标
2681
- .replace(/<img[^>]*>/gi, '[图片]') // 图片替换为文字
2682
- .replace(/<br\s*\/?>/gi, '\n') // <br> 转换为换行
2683
- .replace(/<[^>]+>/g, '') // 移除所有 HTML 标签
2684
- // emoji 和特殊符号过滤
2685
- .replace(/[\u{1F300}-\u{1FAFF}]/gu, '') // 全部 Emoji 范围
2686
- .replace(/[\u{2600}-\u{27BF}]/gu, '') // 杂项/装饰符号
2687
- .replace(/[\u{FE00}-\u{FE0F}]/gu, '') // 变体选择符
2688
- .replace(/[\u{200D}]/gu, '') // ZWJ 零宽连接符
2689
- .replace(/[\u{20E3}]/gu, '') // 组合符号
2690
- .replace(/[\u{2300}-\u{23FF}]/gu, '') // 技术符号
2691
- .replace(/[\u{2B50}-\u{2B55}]/gu, '') // 星星等
2692
- .replace(/[\u{203C}-\u{3299}]/gu, '') // CJK 符号
2693
- .replace(/[\u{E0020}-\u{E007F}]/gu, '') // 标签
2694
- .replace(/[✅❌⚠️🔄⏰🔒💻🔍📁🧠🌐🛠️👋🤖🎯💡🚀👍🎯📊📝🔊🔍💬📌✨✓✗→←↓↑⏹⬇⬆↩]/g, '') // 常用图标
2695
- .replace(/```[\s\S]*?```/g, '代码块') // 代码块替换为文字
2696
- .replace(/`[^`]+`/g, function(m) { return m.slice(1,-1); }) // 保留内联代码文字但去引号
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
- // Auto-play TTS if enabled (skip command execution results)
847
- if (ttsManager.enabled && fullResponse && !fullResponse.match(/^\s*[✅❌⏰]\s*\[执行结果\]/m)) {
848
- const idx = state.messages.length - 1;
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') {