myagent-ai 1.19.9 → 1.20.1

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.
@@ -60,8 +60,8 @@ class MainAgent(BaseAgent):
60
60
  <next_step>当 finish=false 时必填,描述下一步计划做什么(简洁明了,1-2句话)。finish=true 时为空。</next_step>
61
61
  </output>
62
62
 
63
- 事项注意:
64
- 1. toolstocal标签: 可列出所有需要执行的工具调用,可以多个工具。解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调回调大模型,让大模型分析为什么超时,改用其他工具。
63
+ 注意事项:
64
+ 1. toolstocal标签: 尽量一次性列出所有需执行工具调用的,多个"tool""工具调用只要按顺序重复堆叠tool标签即可,解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调回调大模型,让大模型分析为什么超时,改用其他工具。如非必要,不要一次仅调用一个工具。
65
65
  2. 上下文中的记忆系统说明
66
66
  - <automemory>: 系统自动根据你通过 <remember> 保存的记忆和当前用户输入,搜索出的 top10 相关记忆。这些是你过去主动记住的内容(包含时间信息),可供参考。
67
67
  - <recall_memory>: 你在上一轮通过 <recall> 指定的记忆搜索结果。系统根据你提供的关键字和时间点搜索了 top5 相关记忆。
@@ -107,8 +107,7 @@ DEPENDENCIES: List[DepInfo] = [
107
107
  note="PyTorch 音频处理库 (SenseVoice 必需)"),
108
108
  DepInfo("funasr", "funasr", "1.1.0", "stt", "all",
109
109
  note="[v1.18.8] SenseVoice 中文语音识别(首选,阿里达摩院)"),
110
- DepInfo("faster_whisper", "faster-whisper", "1.0.0", "stt", "all",
111
- note="Whisper 本地语音识别引擎 (备选,需 C++ 编译)"),
110
+
112
111
  DepInfo("speech_recognition", "SpeechRecognition", "3.10.0", "stt", "all",
113
112
  note="在线语音识别 (Google API,纯 Python 无需编译,Termux 兼容)"),
114
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.19.9",
3
+ "version": "1.20.1",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -6,5 +6,5 @@
6
6
  funasr>=1.1.0
7
7
  torch>=2.0.0
8
8
  torchaudio>=2.0.0
9
- faster-whisper>=1.0.0
9
+
10
10
  pydub>=0.25.1
package/setup.py CHANGED
@@ -41,7 +41,6 @@ setup(
41
41
  "funasr>=1.1.0",
42
42
  "torch>=2.0.0",
43
43
  "torchaudio>=2.0.0",
44
- "faster-whisper>=1.0.0",
45
44
  # 浏览器自动化 (ChromeDev MCP, 无需 Playwright)
46
45
  # 桌面 GUI 自动化 (内置技能)
47
46
  "pynput>=1.7.6",
@@ -53,7 +52,7 @@ setup(
53
52
  "discord": ["discord.py>=2.3.0"],
54
53
  "anthropic": ["anthropic>=0.18.0"],
55
54
  "communication": ["cryptography>=41.0.0", "websockets>=12.0"],
56
- "voice": ["funasr>=1.1.0", "torch>=2.0.0", "torchaudio>=2.0.0", "faster-whisper>=1.0.0"],
55
+ "voice": ["funasr>=1.1.0", "torch>=2.0.0", "torchaudio>=2.0.0"],
57
56
  "all": [
58
57
  "python-telegram-bot>=21.0",
59
58
  "discord.py>=2.3.0",
@@ -63,7 +62,6 @@ setup(
63
62
  "funasr>=1.1.0",
64
63
  "torch>=2.0.0",
65
64
  "torchaudio>=2.0.0",
66
- "faster-whisper>=1.0.0",
67
65
  ],
68
66
  },
69
67
  entry_points={
package/web/api_server.py CHANGED
@@ -1549,11 +1549,10 @@ window.toggleFullscreen = function() {{
1549
1549
 
1550
1550
  接受音频文件(WAV/WEBM/OGG),使用本地 STT 引擎转录。
1551
1551
  支持的引擎(按优先级):
1552
- 1. [v1.18.7] SenseVoice(推荐,中文识别最佳,需:pip install funasr torch torchaudio)
1553
- 2. faster-whisper(备选,需安装:pip install faster-whisper
1554
- 3. vosk(备选,需安装:pip install vosk)
1555
- 4. LLM API Whisper 兼容端点
1556
- 5. SpeechRecognition(Google,需外网)
1552
+ 1. SenseVoice(推荐,中文识别最佳,需:pip install funasr torch torchaudio)
1553
+ 2. vosk(备选,需安装:pip install vosk
1554
+ 3. LLM API Whisper 兼容端点
1555
+ 4. SpeechRecognition(Google,需外网)
1557
1556
  """
1558
1557
  try:
1559
1558
  reader = await request.multipart()
@@ -1639,117 +1638,6 @@ window.toggleFullscreen = function() {{
1639
1638
  except Exception as e:
1640
1639
  logger.warning(f"SenseVoice 转录失败: {e}")
1641
1640
 
1642
- # ── 尝试 faster-whisper ──
1643
- try:
1644
- whisper_model = self._whisper_model
1645
- if whisper_model is None:
1646
- # 预加载未完成或未安装,尝试懒加载
1647
- import warnings as _w
1648
- _w.filterwarnings("ignore", message=".*HF_TOKEN.*", category=UserWarning)
1649
- _w.filterwarnings("ignore", message=".*huggingface_hub.*token.*", category=UserWarning)
1650
- _w.filterwarnings("ignore", message=".*ffmpeg or avconv.*", category=RuntimeWarning)
1651
- os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
1652
- os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
1653
- os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
1654
- from faster_whisper import WhisperModel
1655
- model_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'models', 'whisper')
1656
- self._whisper_model = WhisperModel("tiny", device="cpu", compute_type="int8",
1657
- download_root=model_dir)
1658
- whisper_model = self._whisper_model
1659
- logger.info("faster-whisper tiny 模型已加载 (CPU int8)")
1660
-
1661
- # faster-whisper 需要 16kHz WAV
1662
- # [v1.15.8] 使用 pydub+ffmpeg 正确转换 WebM/Opus/OGG 等格式
1663
- wav_buf = io.BytesIO()
1664
- try:
1665
- from pydub import AudioSegment
1666
- audio_buf = io.BytesIO(audio_data)
1667
- seg = AudioSegment.from_file(audio_buf, format=audio_format or "webm")
1668
- seg = seg.set_channels(1).set_frame_rate(16000).set_sample_width(2)
1669
- seg.export(wav_buf, format="wav")
1670
- wav_buf.seek(0)
1671
- except Exception as _pydub_err:
1672
- # pydub 不可用时 fallback:仅处理已是 WAV 的情况
1673
- import wave
1674
- audio_buf = io.BytesIO(audio_data)
1675
- try:
1676
- with wave.open(audio_buf, 'rb') as rf:
1677
- wav_buf = io.BytesIO()
1678
- with wave.open(wav_buf, 'wb') as wf:
1679
- wf.setnchannels(1)
1680
- wf.setsampwidth(2)
1681
- wf.setframerate(16000)
1682
- frames = rf.readframes(rf.getnframes())
1683
- wf.writeframes(frames)
1684
- wav_buf.seek(0)
1685
- except Exception:
1686
- logger.warning(f"音频格式转换失败(pydub: {_pydub_err})")
1687
- return web.json_response({"error": "音频格式不支持,需要 WAV 或安装 pydub+ffmpeg"}, status=400)
1688
-
1689
- wav_buf.seek(0)
1690
- segments, info = whisper_model.transcribe(wav_buf, beam_size=1,
1691
- language="zh",
1692
- initial_prompt="以下是普通话的句子",
1693
- vad_filter=True, vad_parameters=dict(
1694
- min_silence_duration_ms=300))
1695
- text = "".join(seg.text for seg in segments).strip()
1696
-
1697
- if text:
1698
- return web.json_response({"text": text, "engine": "faster-whisper"})
1699
- except ImportError:
1700
- logger.debug("faster-whisper 未安装,尝试自动安装...")
1701
- try:
1702
- from core.deps_checker import ensure_skill_deps
1703
- installed = ensure_skill_deps("stt")
1704
- if installed:
1705
- logger.info("faster-whisper 自动安装成功,重新尝试转录")
1706
- from faster_whisper import WhisperModel
1707
- import os
1708
- model_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'models', 'whisper')
1709
- self._whisper_model = WhisperModel("tiny", device="cpu", compute_type="int8",
1710
- download_root=model_dir)
1711
- whisper_model = self._whisper_model
1712
- # 重新执行转录(跳过上面的 try 已完成的逻辑,直接进入转录流程)
1713
- import io
1714
- # [v1.15.8] 使用 pydub+ffmpeg 正确转换音频格式
1715
- wav_buf = io.BytesIO()
1716
- try:
1717
- from pydub import AudioSegment
1718
- audio_buf = io.BytesIO(audio_data)
1719
- seg = AudioSegment.from_file(audio_buf, format=audio_format or "webm")
1720
- seg = seg.set_channels(1).set_frame_rate(16000).set_sample_width(2)
1721
- seg.export(wav_buf, format="wav")
1722
- wav_buf.seek(0)
1723
- except Exception as _pydub_err2:
1724
- import wave
1725
- audio_buf = io.BytesIO(audio_data)
1726
- try:
1727
- with wave.open(audio_buf, 'rb') as rf:
1728
- wav_buf = io.BytesIO()
1729
- with wave.open(wav_buf, 'wb') as wf:
1730
- wf.setnchannels(1)
1731
- wf.setsampwidth(2)
1732
- wf.setframerate(16000)
1733
- frames = rf.readframes(rf.getnframes())
1734
- wf.writeframes(frames)
1735
- wav_buf.seek(0)
1736
- except Exception:
1737
- logger.warning(f"音频格式转换失败(pydub: {_pydub_err2})")
1738
- return web.json_response({"error": "音频格式不支持"}, status=400)
1739
- wav_buf.seek(0)
1740
- segments, info = whisper_model.transcribe(wav_buf, beam_size=1,
1741
- language="zh",
1742
- initial_prompt="以下是普通话的句子",
1743
- vad_filter=True, vad_parameters=dict(
1744
- min_silence_duration_ms=300))
1745
- text = "".join(seg.text for seg in segments).strip()
1746
- if text:
1747
- return web.json_response({"text": text, "engine": "faster-whisper"})
1748
- except Exception as inst_err:
1749
- logger.warning(f"faster-whisper 自动安装/转录失败: {inst_err}")
1750
- except Exception as e:
1751
- logger.warning(f"faster-whisper 转录失败: {e}")
1752
-
1753
1641
  # ── 尝试 vosk ──
1754
1642
  try:
1755
1643
  import vosk
@@ -1872,9 +1760,8 @@ window.toggleFullscreen = function() {{
1872
1760
  "error": "未检测到可用的 STT 引擎。请尝试以下方案:\n"
1873
1761
  " 1. pip install funasr torch torchaudio (SenseVoice,中文最佳,推荐)\n"
1874
1762
  " 2. 配置支持 Whisper 的 LLM API(自动使用,无需安装)\n"
1875
- " 3. pip install faster-whisper (离线本地,需 C++ 编译环境)\n"
1876
- " 4. pip install vosk (离线本地,需下载模型)\n"
1877
- " 5. pip install SpeechRecognition (需外网,国内不可用)",
1763
+ " 3. pip install vosk (离线本地,需下载模型)\n"
1764
+ " 4. pip install SpeechRecognition (需外网,国内不可用)",
1878
1765
  "available": False,
1879
1766
  }, status=503)
1880
1767
 
@@ -2134,6 +2021,23 @@ window.toggleFullscreen = function() {{
2134
2021
  "status": "done",
2135
2022
  })
2136
2023
 
2024
+ # ── 硬上限:任务列表不允许超过 MAX_TASK_ITEMS 条 ──
2025
+ # 系统提示词虽然要求 LLM 精简到 8 条,但 LLM 经常忽略,
2026
+ # 加上 merge 保留 done 项,会导致列表无限增长。
2027
+ # 策略:优先保留未完成项 + 最近完成的项,移除最早的已完成项
2028
+ MAX_TASK_ITEMS = 10
2029
+ if len(merged) > MAX_TASK_ITEMS:
2030
+ # 分离未完成和已完成
2031
+ unfinished = [t for t in merged if t.get("status") != "done"]
2032
+ finished = [t for t in merged if t.get("status") == "done"]
2033
+ # 保留所有未完成项 + 尽可能多的已完成项(保留最新的)
2034
+ remaining = MAX_TASK_ITEMS - len(unfinished)
2035
+ if remaining > 0:
2036
+ finished = finished[-remaining:] # 保留最近完成的
2037
+ else:
2038
+ finished = []
2039
+ merged = unfinished + finished
2040
+
2137
2041
  return merged
2138
2042
 
2139
2043
  async def handle_get_task_plan(self, request):
@@ -6772,8 +6676,8 @@ window.toggleFullscreen = function() {{
6772
6676
  except Exception:
6773
6677
  pass
6774
6678
 
6775
- # [v1.18.8] 后台预加载 STT 模型,避免首次语音识别时等待数秒
6776
- # 优先加载 SenseVoice(中文识别最佳),失败时回退到 faster-whisper
6679
+ # 后台预加载 STT 模型,避免首次语音识别时等待数秒
6680
+ # 仅加载 SenseVoice(中文识别最佳)
6777
6681
  try:
6778
6682
  import threading
6779
6683
  def _preload_stt():
@@ -6784,30 +6688,16 @@ window.toggleFullscreen = function() {{
6784
6688
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
6785
6689
  os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
6786
6690
 
6787
- # 首选: SenseVoice (funasr)
6788
6691
  try:
6789
6692
  from funasr import AutoModel
6790
6693
  model_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'models', 'sensevoice')
6791
6694
  self._sensevoice_model = AutoModel(model="iic/SenseVoiceSmall", model_dir=model_dir,
6792
6695
  device="cpu", disable_pbar=True, disable_update=True)
6793
- logger.info("STT SenseVoice 模型预加载完成 (首选引擎)")
6794
- return # 成功则不加载 whisper
6795
- except ImportError:
6796
- logger.debug("SenseVoice (funasr) 未安装,尝试 faster-whisper")
6797
- except Exception as e:
6798
- logger.debug(f"SenseVoice 预加载失败: {e},尝试 faster-whisper")
6799
-
6800
- # 备选: faster-whisper
6801
- try:
6802
- from faster_whisper import WhisperModel
6803
- model_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'models', 'whisper')
6804
- self._whisper_model = WhisperModel("tiny", device="cpu", compute_type="int8",
6805
- download_root=model_dir)
6806
- logger.info("STT faster-whisper 模型预加载完成 (备选引擎)")
6696
+ logger.info("STT SenseVoice 模型预加载完成")
6807
6697
  except ImportError:
6808
- logger.debug("faster-whisper 未安装,跳过 STT 预加载")
6698
+ logger.debug("SenseVoice (funasr) 未安装,跳过 STT 预加载")
6809
6699
  except Exception as e:
6810
- logger.debug(f"STT 模型预加载失败(不影响使用): {e}")
6700
+ logger.debug(f"SenseVoice 预加载失败(不影响使用): {e}")
6811
6701
  except Exception as e:
6812
6702
  logger.debug(f"STT 预加载异常(不影响使用): {e}")
6813
6703
  threading.Thread(target=_preload_stt, daemon=True).start()
@@ -1998,9 +1998,45 @@ function newChat() {
1998
1998
  var _sessionPollTimers = {};
1999
1999
  function _pollSessionCompletion(sessionId) {
2000
2000
  if (_sessionPollTimers[sessionId]) clearInterval(_sessionPollTimers[sessionId]);
2001
+ var _lastMsgCount = state.messages.length;
2001
2002
  _sessionPollTimers[sessionId] = setInterval(async function() {
2002
2003
  try {
2003
2004
  const data = await api('/api/session/status?sid=' + encodeURIComponent(sessionId));
2005
+ // 每次轮询都重新加载消息,实时显示执行过程中产生的中间输出
2006
+ try {
2007
+ const msgs = await api('/api/session/messages?sid=' + encodeURIComponent(sessionId) + '&limit=500');
2008
+ if (Array.isArray(msgs) && msgs.length > 0) {
2009
+ var loaded = msgs.filter(function(m) {
2010
+ return m && (m.role === 'user' || m.role === 'assistant' || m.role === 'tool');
2011
+ }).map(function(m) {
2012
+ var content = (m.content != null) ? String(m.content) : '';
2013
+ var mkey = (m.key || '').toLowerCase();
2014
+ if (m.role === 'assistant' && mkey !== 'tool_call' && mkey !== 'reasoning' && content && content.trim().startsWith('<')) {
2015
+ content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
2016
+ }
2017
+ var mapped = {
2018
+ role: m.role || 'assistant',
2019
+ content: content,
2020
+ time: m.time || m.created_at || '',
2021
+ key: m.key || '',
2022
+ };
2023
+ if (m.images && m.images.length > 0) mapped.images = m.images;
2024
+ if (m.files && m.files.length > 0) mapped.files = m.files;
2025
+ return mapped;
2026
+ });
2027
+ var newMsgs = groupHistoryMessages(loaded);
2028
+ if (newMsgs.length !== _lastMsgCount) {
2029
+ // 只在有新消息时才重新渲染,避免闪烁
2030
+ state.messages = newMsgs;
2031
+ _lastMsgCount = newMsgs.length;
2032
+ renderMessages();
2033
+ // 自动滚动到底部,显示最新输出
2034
+ var mi = document.getElementById('messagesInner');
2035
+ if (mi) mi.scrollTop = mi.scrollHeight;
2036
+ }
2037
+ }
2038
+ } catch (_) {}
2039
+
2004
2040
  if (!data.running || data.done) {
2005
2041
  clearInterval(_sessionPollTimers[sessionId]);
2006
2042
  delete _sessionPollTimers[sessionId];
@@ -2009,7 +2045,7 @@ function _pollSessionCompletion(sessionId) {
2009
2045
  document.getElementById('sendBtn').style.display = '';
2010
2046
  document.getElementById('sendBtn').disabled = !document.getElementById('userInput').value.trim();
2011
2047
  document.getElementById('stopBtn').style.display = 'none';
2012
- // 重新加载消息以获取完整内容
2048
+ // 最终重新加载消息以获取完整内容
2013
2049
  await selectSession(sessionId);
2014
2050
  if (data.error) {
2015
2051
  toast('⚠️ 后台任务完成但出错: ' + data.error, 'error');
@@ -2681,17 +2717,17 @@ function _renderMessagesInner() {
2681
2717
  </details>`;
2682
2718
  })() : '';
2683
2719
  const _isSpeakingThis = ttsManager && ttsManager.isPlaying && ttsManager.currentMsgIndex === i;
2684
- const actionBtns = (!isUser && msg.content) ? `
2720
+ const actionBtns = msg.content ? `
2685
2721
  <div class="msg-actions">
2686
2722
  <button class="msg-action-btn" onclick="copyMessage(${i})" title="复制">
2687
2723
  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
2688
- </button>
2724
+ </button>${!isUser ? `
2689
2725
  <button class="msg-action-btn${_isSpeakingThis ? ' tts-speaking' : ''}" onclick="speakMessage(${i})" title="${_isSpeakingThis ? '停止朗读' : '朗读'}">
2690
2726
  ${_isSpeakingThis
2691
2727
  ? '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>'
2692
2728
  : '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>'
2693
2729
  }
2694
- </button>
2730
+ </button>` : ''}
2695
2731
  </div>` : '';
2696
2732
  const ttsIndicator = _isSpeakingThis ?
2697
2733
  ' <span class="tts-playing-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>' : '';
@@ -1620,6 +1620,15 @@ async function sendMessage(opts) {
1620
1620
  // 任务列表 JSON 直推更新(exec 模式)
1621
1621
  // 如果已收到 finish 标签,忽略后续 task_list_update,防止覆盖已清空的任务列表
1622
1622
  if (evt.tasks && Array.isArray(evt.tasks) && !_finishReceived) {
1623
+ // 前端安全上限:防止服务端限制失效时列表无限增长
1624
+ if (evt.tasks.length > 15) {
1625
+ var unfinished = evt.tasks.filter(function(t) { return t.status !== 'done'; });
1626
+ var finished = evt.tasks.filter(function(t) { return t.status === 'done'; });
1627
+ var remaining = 15 - unfinished.length;
1628
+ if (remaining > 0) finished = finished.slice(-remaining);
1629
+ else finished = [];
1630
+ evt.tasks = unfinished.concat(finished);
1631
+ }
1623
1632
  state.taskItems = evt.tasks;
1624
1633
  renderTaskList();
1625
1634
  var panel = document.getElementById('taskPanel');
package/web/ui/index.html CHANGED
@@ -274,6 +274,25 @@ tr:hover{background:var(--surface2)}
274
274
  /* 任务表格优化 */
275
275
  .table-wrap table{min-width:auto}
276
276
  .table-wrap td[style*="max-width:300px"]{max-width:150px!important;white-space:normal!important;word-break:break-all}
277
+ /* ── 会话管理移动端:隐藏"最后活动"列,操作按钮堆叠 ── */
278
+ #content[data-page="sessions"] .table-wrap th:nth-child(4),
279
+ #content[data-page="sessions"] .table-wrap td:nth-child(4){display:none}
280
+ #content[data-page="sessions"] .table-wrap td:last-child{white-space:normal;min-width:auto}
281
+ #content[data-page="sessions"] .table-wrap td:last-child .btn{display:inline-block;margin:2px}
282
+ /* ── 任务记录移动端:隐藏"来源"列,操作按钮堆叠 ── */
283
+ #content[data-page="tasks"] .table-wrap th:nth-child(3),
284
+ #content[data-page="tasks"] .table-wrap td:nth-child(3){display:none}
285
+ #content[data-page="tasks"] .table-wrap td:last-child{white-space:normal;min-width:auto}
286
+ #content[data-page="tasks"] .table-wrap td:last-child .btn{display:inline-block;margin:2px}
287
+ /* ── 会话查看:消息气泡在窄屏撑满宽度 ── */
288
+ .msg-bubble-wrap{max-width:75%}
289
+ #content[data-page="sessions"] .msg-bubble-wrap{max-width:88%!important}
290
+ /* ── 任务计划卡片移动端堆叠 ── */
291
+ .card .flex.justify-between{flex-direction:column;align-items:flex-start;gap:6px}
292
+ /* ── 时间索引自适应换行 ── */
293
+ .raw-time-nav{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;margin-bottom:8px}
294
+ .raw-time-link{color:var(--accent);text-decoration:none;font-size:12px;padding:2px 6px;border-radius:4px;background:var(--surface2);white-space:nowrap;transition:background .15s}
295
+ .raw-time-link:hover{background:var(--accent);color:#fff}
277
296
  }
278
297
  @media(max-width:480px){
279
298
  .content{padding:12px}
@@ -287,6 +306,12 @@ tr:hover{background:var(--surface2)}
287
306
  .form-group{margin-bottom:10px}
288
307
  .form-group label{font-size:12px}
289
308
  .raw-collapsed{max-height:0 !important;padding:0 10px !important;overflow:hidden}
309
+ /* ── 480px:任务表进一步精简,隐藏任务ID列 ── */
310
+ #content[data-page="tasks"] .table-wrap th:nth-child(1),
311
+ #content[data-page="tasks"] .table-wrap td:nth-child(1){display:none}
312
+ /* ── 480px:会话表隐藏 Agent 列 ── */
313
+ #content[data-page="sessions"] .table-wrap th:nth-child(2),
314
+ #content[data-page="sessions"] .table-wrap td:nth-child(2){display:none}
290
315
  }
291
316
  </style>
292
317
  </head>
@@ -443,6 +468,7 @@ function showConfirm(title,msg,onOk){
443
468
  function showPage(page, addHistory){
444
469
  closeMobileSidebar();
445
470
  currentPage=page;
471
+ $('content').setAttribute('data-page',page);
446
472
  document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
447
473
  $('pageTitle').textContent=pages[page]||page;
448
474
  const renderers={dashboard:renderDashboard,agents:renderAgents,platforms:renderPlatforms,organization:renderOrganization,departments:renderDepartments,sessions:renderSessions,memory:renderMemory,permissions:renderPermissions,llm:renderLLM,system:renderSystem,executor:renderExecutor,skills:renderSkills,files:renderFiles,logs:renderLogs,tasks:renderTasks};
@@ -468,6 +494,7 @@ function navigateTo(page, sub, renderFn){
468
494
  if(_navHistory.length>6)_navHistory.shift();
469
495
  currentPage=page;
470
496
  _navSubState=sub;
497
+ $('content').setAttribute('data-page',page);
471
498
  // 更新 URL hash
472
499
  var hash=page+(sub?'~'+sub:'');
473
500
  history.pushState({page:page,sub:sub},'','#'+hash);
@@ -1073,7 +1100,7 @@ async function viewSessionMsgs(sid){
1073
1100
  const displayContent=content.length>500?content.slice(0,500)+'...':content;
1074
1101
  html+=`<div style="margin:8px 0;display:flex;gap:8px;${align}">`;
1075
1102
  if(!isUser)html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
1076
- html+=`<div style="max-width:75%;min-width:0"><div style="font-size:11px;color:var(--text3);margin-bottom:2px">${isUser?'用户':'助手'} <span style="margin-left:4px">${time}</span></div><div style="background:${bg};color:${fg};padding:10px 14px;border-radius:var(--radius);font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word">${escHtml(displayContent)}</div></div>`;
1103
+ html+=`<div class="msg-bubble-wrap" style="min-width:0"><div style="font-size:11px;color:var(--text3);margin-bottom:2px">${isUser?'用户':'助手'} <span style="margin-left:4px">${time}</span></div><div style="background:${bg};color:${fg};padding:10px 14px;border-radius:var(--radius);font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word">${escHtml(displayContent)}</div></div>`;
1077
1104
  if(isUser)html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
1078
1105
  html+=`</div>`;
1079
1106
  }
@@ -1311,7 +1338,7 @@ async function _loadSessionMessages(){
1311
1338
  const displayContent=long?content.slice(0,500)+'... (共'+content.length+'字符)':content;
1312
1339
  html+=`<div style="margin:8px 0;display:flex;gap:8px;${isUser?'flex-direction:row-reverse':''}">`;
1313
1340
  html+=`<div style="flex-shrink:0;font-size:16px;margin-top:2px">${avatar}</div>`;
1314
- html+=`<div style="max-width:75%;min-width:0"><div style="font-size:11px;color:var(--text3);margin-bottom:2px;${isUser?'text-align:right':''}">${isUser?'用户':'助手'} <span style="margin-left:4px">${time}</span></div>`;
1341
+ html+=`<div class="msg-bubble-wrap" style="min-width:0"><div style="font-size:11px;color:var(--text3);margin-bottom:2px;${isUser?'text-align:right':''}">${isUser?'用户':'助手'} <span style="margin-left:4px">${time}</span></div>`;
1315
1342
  html+=`<div style="background:${bg};color:${fg};padding:10px 14px;border-radius:var(--radius);font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word">${escHtml(displayContent)}</div>`;
1316
1343
  if(long)html+=`<button class="btn btn-sm btn-ghost" style="margin-top:4px;font-size:11px" onclick="this.previousElementSibling.textContent=this.dataset.full;this.remove()" data-full="${escHtml(content).replace(/"/g,'&quot;')}">展开全部 (${content.length}字符)</button>`;
1317
1344
  html+=`</div></div>`;
@@ -1351,7 +1378,7 @@ async function viewSessionRaw(sid){
1351
1378
  }
1352
1379
  html+=`</div>`;
1353
1380
  // 时间索引导航
1354
- html+=`<div style="margin-bottom:8px;font-size:12px;color:var(--text3)" id="rawTimeNav"></div>`;
1381
+ html+=`<div class="raw-time-nav" id="rawTimeNav"></div>`;
1355
1382
  // 消息列表
1356
1383
  html+=`<div style="max-height:65vh;overflow-y:auto;font-family:monospace" id="rawMsgList">`;
1357
1384
  for(let i=0;i<msgs.length;i++){
@@ -1419,9 +1446,9 @@ function _buildTimeNav(msgs){
1419
1446
  }
1420
1447
  const times=Object.keys(minutes);
1421
1448
  if(times.length<=1)return;
1422
- let navHtml='时间索引: ';
1449
+ let navHtml='<span style="color:var(--text3);font-size:12px">时间索引:</span> ';
1423
1450
  for(const t of times){
1424
- navHtml+=`<a href="javascript:void(0)" style="color:var(--accent);margin-right:8px;text-decoration:none" onclick="document.querySelectorAll('.raw-item')[${minutes[t][0]}].scrollIntoView({behavior:'smooth',block:'center'})">${escHtml(t.slice(11))}</a>`;
1451
+ navHtml+=`<a href="javascript:void(0)" class="raw-time-link" onclick="document.querySelectorAll('.raw-item')[${minutes[t][0]}].scrollIntoView({behavior:'smooth',block:'center'})">${escHtml(t.slice(11))}</a>`;
1425
1452
  }
1426
1453
  nav.innerHTML=navHtml;
1427
1454
  }