myagent-ai 1.8.2 → 1.8.3
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/core/update_manager.py +168 -57
- package/package.json +1 -1
- package/web/api_server.py +76 -3
- package/web/ui/chat/chat.css +11 -0
- package/web/ui/chat/chat_container.html +2 -2
- package/web/ui/chat/chat_main.js +24 -1
- package/web/ui/chat/flow_engine.js +5 -1
- package/web/ui/chat/groupchat.js +2 -0
- package/web/ui/index.html +2 -0
package/core/update_manager.py
CHANGED
|
@@ -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"
|
|
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=
|
|
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
|
-
|
|
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
|
|
484
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
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
|
-
#
|
|
2911
|
-
|
|
2912
|
-
|
|
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:
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -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=
|
|
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=
|
|
34
|
+
<script src="chat.js?v=3"></script>
|
|
35
35
|
</body>
|
|
36
36
|
</html>
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -1096,6 +1096,8 @@ 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) {
|
|
@@ -1608,6 +1610,8 @@ function newChat() {
|
|
|
1608
1610
|
updateSidebarAgentIndicator();
|
|
1609
1611
|
const userInput = document.getElementById('userInput');
|
|
1610
1612
|
if (userInput) userInput.focus();
|
|
1613
|
+
// 移动端:新建会话后自动收起侧边栏
|
|
1614
|
+
if (isMobile()) closeMobileSidebar();
|
|
1611
1615
|
}
|
|
1612
1616
|
|
|
1613
1617
|
async function selectSession(id) {
|
|
@@ -1913,7 +1917,7 @@ function renderMessages() {
|
|
|
1913
1917
|
html += `
|
|
1914
1918
|
<div class="message-row ${msg.role}">
|
|
1915
1919
|
<div class="message-avatar">${avatar}</div>
|
|
1916
|
-
<div style="flex:1;min-width:0">
|
|
1920
|
+
<div class="message-content" style="flex:1;min-width:0">
|
|
1917
1921
|
${reasoningHtml}
|
|
1918
1922
|
${thoughtHtml}
|
|
1919
1923
|
${timelineHtml}
|
|
@@ -2904,6 +2908,25 @@ const ttsManager = {
|
|
|
2904
2908
|
this.updatePlayingIndicator();
|
|
2905
2909
|
},
|
|
2906
2910
|
|
|
2911
|
+
/**
|
|
2912
|
+
* 清除当前流式 TTS 状态(用于多轮迭代之间的 clear_text 事件)
|
|
2913
|
+
* 停止当前播放,清空队列和缓冲区,但保持 enabled 和 _streamActive
|
|
2914
|
+
* 这样下一轮迭代的新文本可以重新入队播放
|
|
2915
|
+
*/
|
|
2916
|
+
clearStream() {
|
|
2917
|
+
// 停止当前音频
|
|
2918
|
+
this.audio.pause();
|
|
2919
|
+
this.audio.currentTime = 0;
|
|
2920
|
+
// 清空队列和缓冲区
|
|
2921
|
+
this._streamBuffer = '';
|
|
2922
|
+
this._audioQueue = [];
|
|
2923
|
+
this._audioPlaying = false;
|
|
2924
|
+
this._stopRequested = false;
|
|
2925
|
+
// 保持 _streamActive = true 和 isPlaying = true
|
|
2926
|
+
// 这样后续的 text_delta 会继续往新的缓冲区添加文本
|
|
2927
|
+
// 新的句子会被合成并入队播放
|
|
2928
|
+
},
|
|
2929
|
+
|
|
2907
2930
|
updatePlayingIndicator() {
|
|
2908
2931
|
// Re-render messages to update the playing indicator
|
|
2909
2932
|
if (typeof renderMessages === 'function') {
|
|
@@ -289,7 +289,7 @@ function updateStreamingMessage(msgIdx) {
|
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
const contentArea = targetRow.querySelector(':scope >
|
|
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();
|
package/web/ui/chat/groupchat.js
CHANGED
package/web/ui/index.html
CHANGED
|
@@ -170,6 +170,8 @@ 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}
|
|
173
175
|
.mobile-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:45;display:none}
|
|
174
176
|
.mobile-overlay.active{display:block}
|
|
175
177
|
.header{padding:12px 16px}
|