myagent-ai 1.8.2 → 1.8.4

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.
@@ -325,15 +325,17 @@ class UpdateManager:
325
325
  logger.debug(f"读取 package.json 失败: {e}")
326
326
  return ""
327
327
 
328
+ registry = self._get_npm_registry()
329
+
328
330
  # 直接请求 npm registry HTTP API(不依赖 npm 命令行)
329
331
  try:
330
332
  import urllib.request
331
- url = f"https://registry.npmjs.org/{pkg_name}/latest"
333
+ url = f"{registry}/{pkg_name}/latest"
332
334
  logger.debug(f"检查 npm registry: {url}")
333
335
  loop = asyncio.get_running_loop()
334
336
  data = await loop.run_in_executor(
335
337
  None,
336
- lambda: urllib.request.urlopen(url, timeout=10).read(),
338
+ lambda: urllib.request.urlopen(url, timeout=15).read(),
337
339
  )
338
340
  info = json.loads(data)
339
341
  version = info.get("version", "")
@@ -347,11 +349,13 @@ class UpdateManager:
347
349
  # 回退: 尝试用 npm 命令行检查 (可能 urllib 失败但 npm 命令可用)
348
350
  try:
349
351
  loop = asyncio.get_running_loop()
352
+ npm_view_cmd = ["npm", "view", pkg_name, "version"]
353
+ if registry != "https://registry.npmjs.org":
354
+ npm_view_cmd.extend(["--registry", registry])
350
355
  result = await loop.run_in_executor(
351
356
  None,
352
357
  lambda: subprocess.run(
353
- ["npm", "view", pkg_name, "version", "--registry",
354
- "https://registry.npmjs.org/"],
358
+ npm_view_cmd,
355
359
  capture_output=True, text=True, timeout=15,
356
360
  )
357
361
  )
@@ -446,7 +450,7 @@ class UpdateManager:
446
450
 
447
451
  try:
448
452
  # ── Phase 1: 安全门控 - 暂停 Agent + 排空队列 ──
449
- await self._enter_safe_mode()
453
+ await self._enter_safe_mode(update_type)
450
454
 
451
455
  # ── Phase 2: 执行更新 ──
452
456
  if update_type == UpdateType.CODE:
@@ -480,24 +484,42 @@ class UpdateManager:
480
484
  # 保存记录
481
485
  self._save_record(record)
482
486
  self._current_update = None
483
- # 恢复 Agent(FULL 类型不需要,因为进程会重启)
484
- if update_type != UpdateType.FULL:
487
+ # 恢复 Agent
488
+ # 注意: FULL 类型成功时 os.execv 会替换进程,finally 不会执行
489
+ # 只有失败时才会走到这里,所以必须恢复 Agent
490
+ try:
485
491
  await self._exit_safe_mode()
492
+ except Exception as exit_err:
493
+ logger.error(f"退出安全模式失败: {exit_err}")
486
494
 
487
495
  return record
488
496
 
489
- async def _enter_safe_mode(self):
497
+ async def _enter_safe_mode(self, update_type: UpdateType = UpdateType.CONFIG):
490
498
  """
491
499
  进入安全模式: 暂停所有 Agent 任务 + 排空任务队列。
492
500
  利用现有的 ConfigBroadcaster 和 TaskQueue 机制。
501
+
502
+ Args:
503
+ update_type: 更新类型,影响 Agent 等待超时时间
504
+ FULL 类型给 Agent 120s 等待,其他 30s
493
505
  """
494
506
  self._status = UpdateStatus.DRAINING
495
507
  logger.info("进入更新安全模式...")
496
508
 
509
+ # 映射 UpdateType → ReloadType,确保 Agent 等待正确的超时时间
510
+ from core.config_broadcast import ReloadType
511
+ reload_type_map = {
512
+ UpdateType.CONFIG: ReloadType.CONFIG,
513
+ UpdateType.CODE: ReloadType.CODE,
514
+ UpdateType.DEPENDENCY: ReloadType.DEPENDENCY,
515
+ UpdateType.FULL: ReloadType.FULL,
516
+ }
517
+ reload_type = reload_type_map.get(update_type, ReloadType.CONFIG)
518
+
497
519
  # Step 1: 通过 ConfigBroadcaster 请求所有 Agent 暂停
498
520
  if self.config_broadcaster:
499
- paused_count = await self.config_broadcaster.request_reload()
500
- logger.info(f"已通知 {paused_count} 个 Agent 暂停")
521
+ paused_count = await self.config_broadcaster.request_reload(reload_type=reload_type)
522
+ logger.info(f"已通知 {paused_count} 个 Agent 暂停 (类型={reload_type.value})")
501
523
 
502
524
  # Step 2: 排空任务队列(等待运行中任务完成,拒绝新任务)
503
525
  if self.task_queue:
@@ -584,68 +606,32 @@ class UpdateManager:
584
606
 
585
607
  async def _do_full_update(self, record: UpdateRecord):
586
608
  """
587
- 全量更新: npm update -g (npm 安装) 或 git pull + pip install (源码安装) + 优雅重启。
609
+ 全量更新: npm install -g (npm 安装) 或 git pull + pip install (源码安装) + 优雅重启。
588
610
  使用 os.execv 替换当前进程,保持 PID 不变。
589
611
  """
590
612
  self._status = UpdateStatus.UPDATING
591
613
  logger.info("开始全量更新...")
592
614
 
593
- is_npm_install = (PROJECT_ROOT / "package.json").exists()
615
+ is_npm_install = self._is_npm_install()
594
616
 
595
617
  if is_npm_install:
596
618
  # npm 安装: npm install -g myagent-ai
597
- self._status = UpdateStatus.DOWNLOADING
598
- loop = asyncio.get_running_loop()
599
- result = await loop.run_in_executor(
600
- None,
601
- lambda: subprocess.run(
602
- ["npm", "install", "-g", "myagent-ai"],
603
- capture_output=True, text=True, timeout=180,
604
- )
605
- )
606
- npm_output = result.stdout + result.stderr
607
- record.details["update_output"] = npm_output[:2000]
608
- logger.info(f"npm update: {npm_output[:500]}")
609
-
610
- if result.returncode != 0:
611
- # npm 返回非零码可能是权限问题或网络问题
612
- error_msg = npm_output[-500:] if npm_output else "未知错误"
613
- # 尝试判断是否真的失败了(npm 有时返回非零但实际安装成功)
614
- if "ERR!" in error_msg or "error" in error_msg.lower():
615
- logger.warning(f"npm install 可能失败: {error_msg}")
616
- # 不直接抛异常,让重启后 start.js 的依赖检查来兜底
617
- else:
618
- logger.warning(f"npm install 有警告但不影响: {error_msg}")
619
+ await self._npm_global_update(record)
619
620
  else:
620
621
  # 源码安装: git pull + pip install
621
- result = subprocess.run(
622
- ["git", "pull"],
623
- capture_output=True, text=True, timeout=30,
624
- cwd=PROJECT_ROOT,
625
- )
626
- pull_output = result.stdout + result.stderr
627
- logger.info(f"git pull: {pull_output[:500]}")
622
+ await self._source_update(record)
628
623
 
629
- self._status = UpdateStatus.DOWNLOADING
630
- loop = asyncio.get_running_loop()
631
- result = await loop.run_in_executor(
632
- None,
633
- lambda: subprocess.run(
634
- [sys.executable, "-m", "pip", "install", "-e", ".",
635
- "--quiet"],
636
- capture_output=True, text=True, timeout=120,
637
- cwd=PROJECT_ROOT,
638
- )
639
- )
640
- pip_output = result.stdout + result.stderr
641
- record.details["update_output"] = pip_output[:1000]
642
- if result.returncode != 0:
643
- logger.warning(f"pip install 有警告: {pip_output[-300:]}")
624
+ # 验证更新是否成功(读取更新后的 package.json 版本)
625
+ new_version = get_version()
626
+ if new_version == record.from_version and record.to_version != "latest":
627
+ # 版本没变,更新可能未生效
628
+ logger.warning(f"更新后版本仍为 {new_version},可能与预期不符")
644
629
 
645
630
  # Step 3: 保存更新完成标记(用于重启后验证)
646
631
  record.status = "restarting"
632
+ record.to_version = new_version
647
633
  self._save_record(record)
648
- logger.info("全量更新完成,准备优雅重启...")
634
+ logger.info(f"全量更新完成 v{record.from_version} → v{new_version},准备优雅重启...")
649
635
 
650
636
  # Step 4: 优雅重启
651
637
  self._status = UpdateStatus.RESTARTING
@@ -697,6 +683,131 @@ class UpdateManager:
697
683
  logger.info(f"通过 Python 直接重启: {sys.executable} {abs_main}")
698
684
  os.execv(sys.executable, [sys.executable, abs_main] + sys.argv[1:])
699
685
 
686
+ def _is_npm_install(self) -> bool:
687
+ """
688
+ 判断是否通过 npm 全局安装。
689
+ 不仅检查 package.json 是否存在(source 安装也有),
690
+ 还检查是否位于 npm 全局目录中。
691
+ """
692
+ pkg_json = PROJECT_ROOT / "package.json"
693
+ if not pkg_json.exists():
694
+ return False
695
+
696
+ # 检查是否在 npm 全局目录中
697
+ try:
698
+ result = subprocess.run(
699
+ ["npm", "root", "-g"],
700
+ capture_output=True, text=True, timeout=5,
701
+ )
702
+ npm_root = result.stdout.strip()
703
+ if npm_root:
704
+ # 规范化路径比较
705
+ import pathlib
706
+ project_resolved = PROJECT_ROOT.resolve()
707
+ npm_root_resolved = pathlib.Path(npm_root).resolve()
708
+ # 检查 PROJECT_ROOT 是否在 npm 全局目录下
709
+ try:
710
+ project_resolved.relative_to(npm_root_resolved)
711
+ return True
712
+ except ValueError:
713
+ pass
714
+
715
+ # 也检查包名子目录 (npm root -g / myagent-ai)
716
+ pkg_dir = npm_root_resolved / "myagent-ai"
717
+ if project_resolved == pkg_dir:
718
+ return True
719
+ except (subprocess.TimeoutExpired, FileNotFoundError):
720
+ pass
721
+
722
+ # npm 命令不可用时,检查是否有 .git 目录来区分
723
+ # 有 .git = 源码安装,没有 = 可能是 npm 安装
724
+ if (PROJECT_ROOT / ".git").exists():
725
+ return False
726
+
727
+ # 默认假设 npm 安装(保守策略)
728
+ logger.debug("无法确定安装方式,默认使用 npm 更新路径")
729
+ return True
730
+
731
+ def _get_npm_registry(self) -> str:
732
+ """获取 npm registry 地址,国内用户自动使用镜像"""
733
+ import locale
734
+ lang = (os.environ.get("LANG", "") or os.environ.get("LC_ALL", "") or "").lower()
735
+ env_mirror = os.environ.get("MYAGENT_NPM_MIRROR", "")
736
+ if env_mirror in ("1", "true", "cn"):
737
+ return "https://registry.npmmirror.com"
738
+ if lang and ("zh_cn" in lang or "chinese" in lang):
739
+ return "https://registry.npmmirror.com"
740
+ # 通过 IP 地址判断(可选)
741
+ return "https://registry.npmjs.org"
742
+
743
+ async def _npm_global_update(self, record: UpdateRecord):
744
+ """执行 npm 全局包更新"""
745
+ self._status = UpdateStatus.DOWNLOADING
746
+ registry = self._get_npm_registry()
747
+ loop = asyncio.get_running_loop()
748
+
749
+ pkg_data = json.loads((PROJECT_ROOT / "package.json").read_text(encoding="utf-8"))
750
+ pkg_name = pkg_data.get("name", "myagent-ai")
751
+
752
+ # 构建 npm install 命令
753
+ npm_cmd = ["npm", "install", "-g", pkg_name]
754
+ if registry != "https://registry.npmjs.org":
755
+ npm_cmd.extend(["--registry", registry])
756
+ logger.info(f"使用 npm 镜像: {registry}")
757
+
758
+ result = await loop.run_in_executor(
759
+ None,
760
+ lambda: subprocess.run(
761
+ npm_cmd,
762
+ capture_output=True, text=True, timeout=300,
763
+ )
764
+ )
765
+ npm_output = result.stdout + result.stderr
766
+ record.details["update_output"] = npm_output[:2000]
767
+ logger.info(f"npm update: {npm_output[:500]}")
768
+
769
+ if result.returncode != 0:
770
+ error_msg = npm_output[-500:] if npm_output else "未知错误"
771
+ # 检查是否包含真正的错误(npm 有时返回非零码但有警告信息)
772
+ has_real_error = False
773
+ for line in error_msg.split("\n"):
774
+ stripped = line.strip()
775
+ if stripped.startswith("ERR!") or "code E" in stripped:
776
+ has_real_error = True
777
+ break
778
+ if has_real_error:
779
+ raise RuntimeError(f"npm install -g {pkg_name} 失败:\n{error_msg}")
780
+ else:
781
+ logger.warning(f"npm install 有警告但可能已成功: {error_msg}")
782
+
783
+ async def _source_update(self, record: UpdateRecord):
784
+ """执行源码安装更新 (git pull + pip install)"""
785
+ # git pull
786
+ result = subprocess.run(
787
+ ["git", "pull"],
788
+ capture_output=True, text=True, timeout=30,
789
+ cwd=PROJECT_ROOT,
790
+ )
791
+ pull_output = result.stdout + result.stderr
792
+ logger.info(f"git pull: {pull_output[:500]}")
793
+
794
+ # pip install
795
+ self._status = UpdateStatus.DOWNLOADING
796
+ loop = asyncio.get_running_loop()
797
+ result = await loop.run_in_executor(
798
+ None,
799
+ lambda: subprocess.run(
800
+ [sys.executable, "-m", "pip", "install", "-e", ".",
801
+ "--quiet"],
802
+ capture_output=True, text=True, timeout=120,
803
+ cwd=PROJECT_ROOT,
804
+ )
805
+ )
806
+ pip_output = result.stdout + result.stderr
807
+ record.details["update_output"] = pip_output[:1000]
808
+ if result.returncode != 0:
809
+ raise RuntimeError(f"pip install 失败: {pip_output[-300:]}")
810
+
700
811
  async def _do_config_update(self, record: UpdateRecord):
701
812
  """仅配置热重载(利用现有 ConfigManager.reload())"""
702
813
  self._status = UpdateStatus.RELOADING
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -865,6 +865,77 @@ class ApiServer:
865
865
  tasks.append({"text": text, "status": status})
866
866
  return tasks if tasks else None
867
867
 
868
+ def _merge_task_list(self, session_id: str, llm_task_list: list) -> list:
869
+ """
870
+ 合并 LLM 输出的 tasklist 与服务端存储的现有任务状态。
871
+
872
+ 核心规则:
873
+ 1. 服务端通过执行成功标记为 done 的任务,不会被 LLM 覆盖回 pending/running
874
+ 2. LLM 可以将 pending → running, pending/running → done(正常推进)
875
+ 3. LLM 新增的任务直接添加
876
+ 4. 保留服务端存储中 LLM 没提到的已完成任务
877
+
878
+ 这解决了 LLM 多轮对话中忘记已完成任务、导致循环重复执行的问题。
879
+ """
880
+ stored = self._task_list_store.get(session_id, [])
881
+
882
+ # 构建"服务端确认完成"集合:通过代码执行成功标记为 done 的任务
883
+ # 这些任务的 text 和 status 都来自服务端逻辑(_execute_actions_streaming 之后的状态更新)
884
+ server_done_texts = set()
885
+ for t in stored:
886
+ if t.get("status") == "done":
887
+ server_done_texts.add(t.get("text", "").strip())
888
+
889
+ # 合并结果
890
+ merged = []
891
+ matched_stored_indices = set()
892
+
893
+ for llm_task in llm_task_list:
894
+ llm_text = llm_task.get("text", "").strip()
895
+ llm_status = llm_task.get("status", "pending")
896
+
897
+ # 查找匹配的已存储任务(按文本内容匹配)
898
+ best_idx = -1
899
+ for i, st in enumerate(stored):
900
+ if i in matched_stored_indices:
901
+ continue
902
+ if st.get("text", "").strip() == llm_text:
903
+ best_idx = i
904
+ break
905
+
906
+ if best_idx >= 0:
907
+ matched_stored_indices.add(best_idx)
908
+ stored_status = stored[best_idx].get("status", "pending")
909
+
910
+ # 关键保护:服务端已标记 done 的任务不能被 LLM 回退
911
+ if stored_status == "done":
912
+ merged.append({
913
+ "text": llm_text,
914
+ "status": "done",
915
+ })
916
+ else:
917
+ # 允许 LLM 正常推进:pending → running → done
918
+ merged.append({
919
+ "text": llm_text,
920
+ "status": llm_status,
921
+ })
922
+ else:
923
+ # LLM 新增的任务
924
+ merged.append({
925
+ "text": llm_text,
926
+ "status": llm_status,
927
+ })
928
+
929
+ # 保留服务端已 done 但 LLM 没提到的任务(LLM 可能漏掉了)
930
+ for i, st in enumerate(stored):
931
+ if i not in matched_stored_indices and st.get("status") == "done":
932
+ merged.append({
933
+ "text": st.get("text", ""),
934
+ "status": "done",
935
+ })
936
+
937
+ return merged
938
+
868
939
  async def handle_get_task_plan(self, request):
869
940
  """GET /api/task-plan?agent=default - Get task list from memory."""
870
941
  agent_path = request.query.get("agent", "default")
@@ -2907,9 +2978,11 @@ class ApiServer:
2907
2978
  if chat_mode == "exec":
2908
2979
  task_list = self._extract_task_list_json(content)
2909
2980
  if task_list is not None:
2910
- # 保存到内存(按 session 隔离,避免跨会话任务泄漏)
2911
- self._task_list_store[session_id] = task_list
2912
- await _write_sse({"type": "task_list_update", "tasks": task_list})
2981
+ # 合并 LLM 输出的 tasklist 与服务端存储的状态
2982
+ # 关键:服务端标记为 done 的任务不能被 LLM 覆盖回 pending
2983
+ merged = self._merge_task_list(session_id, task_list)
2984
+ self._task_list_store[session_id] = merged
2985
+ await _write_sse({"type": "task_list_update", "tasks": merged})
2913
2986
 
2914
2987
  # ── Check for tool calls (OpenAI function calling) ──
2915
2988
  if response.tool_calls:
@@ -1728,6 +1728,7 @@ input,textarea,select{font:inherit}
1728
1728
  .agent-panel.collapsed .agent-create-btn,
1729
1729
  .agent-panel.collapsed .agent-collapse-btn span,
1730
1730
  .agent-panel.collapsed .agent-panel-footer{display:flex!important}
1731
+ .agent-panel.collapsed .agent-list{flex-direction:column!important}
1731
1732
  .agent-panel.collapsed .agent-collapse-btn span{display:inline!important}
1732
1733
  .agent-panel.mobile-open{
1733
1734
  transform:translateX(0);
@@ -1745,6 +1746,16 @@ input,textarea,select{font:inherit}
1745
1746
  display:flex;
1746
1747
  }
1747
1748
 
1749
+ /* ── Show sidebar footer/version on mobile (even when collapsed from desktop) ── */
1750
+ .sidebar.collapsed .sidebar-footer,
1751
+ .sidebar.collapsed .sidebar-search,
1752
+ .sidebar.collapsed .session-list{
1753
+ display:flex!important;
1754
+ }
1755
+ .sidebar.collapsed .sidebar-search{flex-direction:column}
1756
+ .sidebar.collapsed .session-list{flex-direction:column}
1757
+ .sidebar.collapsed .sidebar-footer{flex-direction:column}
1758
+
1748
1759
  /* ── Header ── */
1749
1760
  .main-header{
1750
1761
  height:50px;min-height:50px;
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>MyAgent - AI 助手</title>
7
- <link rel="stylesheet" href="chat.css?v=3">
7
+ <link rel="stylesheet" href="chat.css?v=4">
8
8
  </head>
9
9
  <body>
10
10
  <div class="app">
@@ -31,6 +31,6 @@
31
31
  <div id="groupModalContainer"></div>
32
32
 
33
33
  <!-- Step 1: Load HTML fragments, then Step 2: Load main logic -->
34
- <script src="chat.js?v=2"></script>
34
+ <script src="chat.js?v=3"></script>
35
35
  </body>
36
36
  </html>
@@ -1096,9 +1096,13 @@ function quickChatAgent(agentPath) {
1096
1096
  loadAgentDetails(agentPath).then(function(details) {
1097
1097
  updateWelcomeCard(agentPath, details);
1098
1098
  });
1099
+ // 移动端:快速对话后自动收起右侧栏
1100
+ if (isMobile()) closeMobileAgentPanel();
1099
1101
  }
1100
1102
 
1101
1103
  async function selectAgent(agentPath) {
1104
+ // 移动端:立即关闭右侧栏,不要等异步操作完成
1105
+ if (isMobile()) closeMobileAgentPanel();
1102
1106
  // Always reload sessions even if clicking the same agent
1103
1107
  state.activeAgent = agentPath;
1104
1108
  StatePersistence.save('activeAgent', agentPath);
@@ -1144,7 +1148,6 @@ async function selectAgent(agentPath) {
1144
1148
  // 如果 loadSessions 已经 auto-selected 了 session,UI 已由 selectSession 设置好,不再覆盖
1145
1149
 
1146
1150
  document.getElementById('userInput').focus();
1147
- if (isMobile()) closeMobileAgentPanel();
1148
1151
  // Reload task plan if in exec mode
1149
1152
  if (state.chatMode === 'exec') loadTaskPlan();
1150
1153
  // Reset escalation and update exec mode UI
@@ -1608,6 +1611,8 @@ function newChat() {
1608
1611
  updateSidebarAgentIndicator();
1609
1612
  const userInput = document.getElementById('userInput');
1610
1613
  if (userInput) userInput.focus();
1614
+ // 移动端:新建会话后自动收起侧边栏
1615
+ if (isMobile()) closeMobileSidebar();
1611
1616
  }
1612
1617
 
1613
1618
  async function selectSession(id) {
@@ -1913,7 +1918,7 @@ function renderMessages() {
1913
1918
  html += `
1914
1919
  <div class="message-row ${msg.role}">
1915
1920
  <div class="message-avatar">${avatar}</div>
1916
- <div style="flex:1;min-width:0">
1921
+ <div class="message-content" style="flex:1;min-width:0">
1917
1922
  ${reasoningHtml}
1918
1923
  ${thoughtHtml}
1919
1924
  ${timelineHtml}
@@ -2904,6 +2909,25 @@ const ttsManager = {
2904
2909
  this.updatePlayingIndicator();
2905
2910
  },
2906
2911
 
2912
+ /**
2913
+ * 清除当前流式 TTS 状态(用于多轮迭代之间的 clear_text 事件)
2914
+ * 停止当前播放,清空队列和缓冲区,但保持 enabled 和 _streamActive
2915
+ * 这样下一轮迭代的新文本可以重新入队播放
2916
+ */
2917
+ clearStream() {
2918
+ // 停止当前音频
2919
+ this.audio.pause();
2920
+ this.audio.currentTime = 0;
2921
+ // 清空队列和缓冲区
2922
+ this._streamBuffer = '';
2923
+ this._audioQueue = [];
2924
+ this._audioPlaying = false;
2925
+ this._stopRequested = false;
2926
+ // 保持 _streamActive = true 和 isPlaying = true
2927
+ // 这样后续的 text_delta 会继续往新的缓冲区添加文本
2928
+ // 新的句子会被合成并入队播放
2929
+ },
2930
+
2907
2931
  updatePlayingIndicator() {
2908
2932
  // Re-render messages to update the playing indicator
2909
2933
  if (typeof renderMessages === 'function') {
@@ -289,7 +289,7 @@ function updateStreamingMessage(msgIdx) {
289
289
  return;
290
290
  }
291
291
 
292
- const contentArea = targetRow.querySelector(':scope > div');
292
+ const contentArea = targetRow.querySelector(':scope > .message-content');
293
293
  if (!contentArea) return;
294
294
 
295
295
  // Update reasoning block (model inference/reasoning - e.g. o1, DeepSeek-R1)
@@ -894,6 +894,10 @@ async function sendMessage() {
894
894
  state.messages[msgIdx]._streamingText = '';
895
895
  state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '';
896
896
  throttledStreamUpdate(msgIdx);
897
+ // 停止当前轮次的 TTS 播放,防止旧迭代语音与新迭代语音互相打断
898
+ if (ttsManager.enabled && ttsManager._streamActive) {
899
+ ttsManager.clearStream();
900
+ }
897
901
  } else if (evt.type === 'exec_event') {
898
902
  // Real-time execution event (tool call, code exec, skill result, etc.)
899
903
  flushCurrentText();
@@ -138,6 +138,8 @@ async function selectGroup(gid) {
138
138
  // Update sidebar
139
139
  renderSessions();
140
140
  renderGroups();
141
+ // 移动端:选择群聊后自动收起右侧栏
142
+ if (typeof isMobile === 'function' && isMobile()) closeMobileAgentPanel();
141
143
 
142
144
  // Load group data
143
145
  try {
package/web/ui/index.html CHANGED
@@ -170,6 +170,13 @@ tr:hover{background:var(--surface2)}
170
170
  .sidebar.collapsed{width:260px;transform:translateX(-100%)}
171
171
  .sidebar.collapsed.mobile-open{transform:translateX(0)}
172
172
  .sidebar-toggle{display:none!important}
173
+ /* 移动端:即使桌面端折叠了侧边栏,打开时也要显示底部版本信息 */
174
+ .sidebar.collapsed.mobile-open .sidebar-footer-text{display:block!important}
175
+ /* 移动端:打开侧边栏时,即使桌面端折叠了,也要显示图标文字 */
176
+ .sidebar.mobile-open .nav-item .icon-text{display:inline!important}
177
+ .sidebar.mobile-open .logo .logo-text{display:inline!important}
178
+ .sidebar.mobile-open.collapsed .nav-item{justify-content:flex-start;padding:8px 12px}
179
+ .sidebar.mobile-open.collapsed .logo{justify-content:flex-start;padding:16px 12px}
173
180
  .mobile-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:45;display:none}
174
181
  .mobile-overlay.active{display:block}
175
182
  .header{padding:12px 16px}