myagent-ai 1.20.2 → 1.20.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.
@@ -74,6 +74,8 @@ class MainAgent(BaseAgent):
74
74
  - **执行命令**: 用 `command` 或 `command_run` 工具
75
75
  - **文件操作**: 用 `file_read` / `file_write` / `file_list` 等文件工具
76
76
  - **发送文件给用户**: 用 `file_send` 工具(参数: file_path=文件路径, description=说明),当你生成或处理了文件需要返回给用户时使用
77
+ - **播放音频**: 用 `playaudio` 工具(参数: url=音乐链接或file_path=本地文件路径),在聊天中内嵌播放音频(支持QQ音乐、YouTube音乐、本地MP3/WAV等),播放时自动关闭语音合成
78
+ - **播放视频**: 用 `playvideo` 工具(参数: url=视频链接或file_path=本地文件路径),在聊天中内嵌播放视频(支持抖音、YouTube、B站、本地MP4等),播放时自动关闭语音合成
77
79
  - **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
78
80
  4. 准备好内容后,最后,再检查输出格式,确保满足以下要求:
79
81
  <output>
@@ -1648,6 +1650,103 @@ class MainAgent(BaseAgent):
1648
1650
  result = {"success": False, "error": f"文件发送失败: {_fse}"}
1649
1651
  logger.warning(f"[{task_id}] file_send 工具异常: {_fse}")
1650
1652
 
1653
+ elif tool_name in ("playaudio", "playvideo"):
1654
+ # [v1.20.3] 音视频播放工具 — 在聊天中内嵌播放器
1655
+ try:
1656
+ _media_url = params.get("url", "").strip()
1657
+ _media_file = params.get("file_path", "").strip()
1658
+ _media_type = "audio" if tool_name == "playaudio" else "video"
1659
+ _embed_url = None
1660
+ _embed_title = params.get("title", "")
1661
+
1662
+ if _media_url:
1663
+ # 在线链接 — 提取嵌入式播放 URL
1664
+ import re
1665
+ _url_lower = _media_url.lower()
1666
+ # YouTube: https://www.youtube.com/watch?v=xxx 或 youtu.be/xxx
1667
+ _yt_match = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([\w-]+)', _media_url)
1668
+ if _yt_match:
1669
+ _embed_url = f"https://www.youtube.com/embed/{_yt_match.group(1)}"
1670
+ _embed_title = _embed_title or "YouTube 视频"
1671
+ # Bilibili: https://www.bilibili.com/video/BVxxx 或 b23.tv/xxx
1672
+ elif 'bilibili.com' in _url_lower or 'b23.tv' in _url_lower:
1673
+ _bv_match = re.search(r'bilibili\.com/video/(BV[\w]+)', _media_url)
1674
+ if _bv_match:
1675
+ _embed_url = f"https://player.bilibili.com/player.html?bvid={_bv_match.group(1)}&autoplay=0"
1676
+ _embed_title = _embed_title or "B站视频"
1677
+ else:
1678
+ _embed_url = _media_url # b23.tv 短链接直接使用
1679
+ _embed_title = _embed_title or "B站视频"
1680
+ # QQ音乐: https://y.qq.com/n/ryqq/songDetail/xxx
1681
+ elif 'y.qq.com' in _url_lower:
1682
+ _embed_url = _media_url
1683
+ _embed_title = _embed_title or "QQ音乐"
1684
+ # 网易云音乐: https://music.163.com/song?id=xxx
1685
+ elif 'music.163.com' in _url_lower:
1686
+ _song_match = re.search(r'music\.163\.com.*[?&]id=(\d+)', _media_url)
1687
+ if _song_match:
1688
+ _embed_url = f"https://music.163.com/outchain/player?type=2&id={_song_match.group(1)}&auto=0&height=66"
1689
+ _embed_title = _embed_title or "网易云音乐"
1690
+ else:
1691
+ _embed_url = _media_url
1692
+ _embed_title = _embed_title or "网易云音乐"
1693
+ # 抖音: https://www.douyin.com/video/xxx
1694
+ elif 'douyin.com' in _url_lower:
1695
+ _embed_url = _media_url
1696
+ _embed_title = _embed_title or "抖音视频"
1697
+ else:
1698
+ # 其他 URL,尝试直接嵌入
1699
+ _embed_url = _media_url
1700
+ _embed_title = _embed_title or ("在线音乐" if _media_type == "audio" else "在线视频")
1701
+
1702
+ if _embed_url and stream_callback:
1703
+ # 在线播放 — 发送 v2_media 事件让前端渲染嵌入播放器
1704
+ stream_callback({
1705
+ "type": "v2_media",
1706
+ "data": {
1707
+ "media_type": _media_type,
1708
+ "embed_url": _embed_url,
1709
+ "title": _embed_title,
1710
+ "original_url": _media_url,
1711
+ }
1712
+ })
1713
+ result = {"success": True, "output": f"已嵌入{_embed_title}播放器: {_media_url}"}
1714
+
1715
+ elif _media_file:
1716
+ # 本地文件 — 使用 file_send 发送文件,前端渲染内嵌播放器
1717
+ from pathlib import Path as _P
1718
+ _fpath = _P(_media_file).expanduser().resolve()
1719
+ if not _fpath.exists():
1720
+ result = {"success": False, "error": f"文件不存在: {_media_file}"}
1721
+ else:
1722
+ from skills.file_send import FileSendSkill
1723
+ _fskill = FileSendSkill()
1724
+ _desc = f"{'音频' if _media_type == 'audio' else '视频'}播放: {_fpath.name}"
1725
+ _fresult = await _fskill.execute(str(_fpath), _desc, stream_callback=stream_callback)
1726
+ if _fresult.get("success"):
1727
+ # 标记为媒体文件,前端渲染内嵌播放器
1728
+ _fresult["_media_type"] = _media_type
1729
+ result = {"success": True, "output": f"已发送{_media_type}文件: {_fpath.name}", "data": _fresult}
1730
+ try:
1731
+ if _fresult.get("file_id"):
1732
+ _sent_files.append({
1733
+ "id": _fresult["file_id"],
1734
+ "name": _fresult.get("name", ""),
1735
+ "type": _fresult.get("type", ""),
1736
+ "size": _fresult.get("size", 0),
1737
+ "_media_type": _media_type,
1738
+ })
1739
+ except NameError:
1740
+ pass
1741
+ else:
1742
+ result = {"success": False, "error": _fresult.get("error", "文件发送失败")}
1743
+ else:
1744
+ result = {"success": False, "error": f"请提供 url(在线链接)或 file_path(本地文件路径)参数"}
1745
+
1746
+ except Exception as _me:
1747
+ result = {"success": False, "error": f"播放工具异常: {_me}"}
1748
+ logger.warning(f"[{task_id}] {tool_name} 工具异常: {_me}")
1749
+
1651
1750
  elif self.skills:
1652
1751
  exec_result = await self.skills.execute(tool_name, **params)
1653
1752
  if exec_result is None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.20.2",
3
+ "version": "1.20.3",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -646,6 +646,40 @@ input,textarea,select{font:inherit}
646
646
  .msg-attachments-files {
647
647
  margin-top:6px; /* 文件在气泡内容下方 */
648
648
  }
649
+ /* [v1.20.3] 媒体嵌入播放器(在线音视频) */
650
+ .msg-attachments-media {
651
+ margin-bottom:10px;
652
+ }
653
+ .msg-media-embed {
654
+ background:var(--bg2);border:1px solid var(--bg4);border-radius:var(--radius-sm);
655
+ overflow:hidden;max-width:640px;
656
+ }
657
+ .msg-media-header {
658
+ display:flex;align-items:center;gap:6px;padding:6px 10px;
659
+ background:var(--bg3);font-size:12px;color:var(--text2);
660
+ }
661
+ .msg-media-icon {font-size:14px}
662
+ .msg-media-label {flex:1;font-weight:500}
663
+ .msg-media-link {
664
+ color:var(--accent);text-decoration:none;font-size:14px;padding:2px 4px;border-radius:4px;
665
+ }
666
+ .msg-media-link:hover {background:var(--accent-light)}
667
+ /* [v1.20.3] 本地音视频播放器 */
668
+ .msg-media-player {
669
+ margin-bottom:8px;
670
+ }
671
+ .msg-media-player audio,
672
+ .msg-media-player video {
673
+ border-radius:8px;
674
+ box-shadow:0 1px 4px rgba(0,0,0,.1);
675
+ }
676
+ .msg-media-title {
677
+ font-size:12px;color:var(--text2);margin-top:4px;padding:0 4px;
678
+ }
679
+ @media(max-width:768px){
680
+ .msg-media-embed iframe { max-width:100%!important; }
681
+ .msg-media-player audio, .msg-media-player video { max-width:100%!important; }
682
+ }
649
683
  .msg-image-wrapper {
650
684
  max-width:300px;border-radius:var(--radius-sm);overflow:hidden;
651
685
  border:1px solid var(--bg4);cursor:pointer;
@@ -2571,6 +2571,22 @@ function groupHistoryMessages(messages) {
2571
2571
  if (execParts.length > 0) entry.exec_events = execParts.map(function(p) { return p.data; });
2572
2572
  // [v1.19.3] 设置收集到的所有 agent 文件
2573
2573
  if (allAgentFiles.length > 0) entry._files = allAgentFiles;
2574
+ // [v1.20.3] 从 playaudio/playvideo 工具调用中重建 _media 数据(历史回放支持)
2575
+ var mediaEmbeds = [];
2576
+ for (var ei = 0; ei < parts.length; ei++) {
2577
+ var ep = parts[ei];
2578
+ if (ep.type === 'exec' && ep.data.tool_name && (ep.data.tool_name === 'playaudio' || ep.data.tool_name === 'playvideo')) {
2579
+ try {
2580
+ var mparams = typeof ep.data.params === 'string' ? JSON.parse(ep.data.params) : (ep.data.params || {});
2581
+ var murl = mparams.url || '';
2582
+ if (murl) {
2583
+ var mtype = ep.data.tool_name === 'playaudio' ? 'audio' : 'video';
2584
+ mediaEmbeds.push({ media_type: mtype, embed_url: murl, title: mparams.title || '', original_url: murl });
2585
+ }
2586
+ } catch(mpe) { /* ignore parse errors */ }
2587
+ }
2588
+ }
2589
+ if (mediaEmbeds.length > 0) entry._media = mediaEmbeds;
2574
2590
 
2575
2591
  grouped.push(entry);
2576
2592
  }
@@ -2691,13 +2707,59 @@ function _renderMessagesInner() {
2691
2707
  if (!isUser && agentFiles.length > 0) {
2692
2708
  for (const f of agentFiles) {
2693
2709
  const isImage = f.type && f.type.startsWith('image/');
2710
+ const isAudio = f.type && f.type.startsWith('audio/');
2711
+ const isVideo = f.type && f.type.startsWith('video/');
2694
2712
  if (isImage && f.id) {
2695
2713
  parts.push('<div class="msg-image-wrapper agent-image"><img src="/api/file/' + f.id + '" class="msg-image" loading="lazy" alt="' + escapeHtml(f.name || 'image') + '" onerror="this.onerror=null;this.style.background=\'var(--bg3)\';this.style.minHeight=\'60px\';this.alt=\'[图片加载失败]\'" onclick="openFileViewer(\'' + f.id + '\', this.src, \'' + escapeHtml(f.name) + '\')" /></div>');
2696
2714
  }
2715
+ // [v1.20.3] 音频文件渲染为内嵌播放器
2716
+ if (isAudio && f.id) {
2717
+ parts.push('<div class="msg-media-player"><audio controls src="/api/file/' + f.id + '" style="width:100%;max-width:480px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></audio><div class="msg-media-title">' + escapeHtml(f.name || '音频') + '</div></div>');
2718
+ }
2719
+ // [v1.20.3] 视频文件渲染为内嵌播放器
2720
+ if (isVideo && f.id) {
2721
+ parts.push('<div class="msg-media-player"><video controls src="/api/file/' + f.id + '" style="width:100%;max-width:640px;border-radius:8px" preload="metadata" onplay="if(typeof muteTTS===\'function\')muteTTS()" onended="if(typeof unmuteTTS===\'function\')unmuteTTS()"></video><div class="msg-media-title">' + escapeHtml(f.name || '视频') + '</div></div>');
2722
+ }
2697
2723
  }
2698
2724
  }
2699
2725
  return parts.length > 0 ? '<div class="msg-attachments msg-attachments-images">' + parts.join('') + '</div>' : '';
2700
2726
  })();
2727
+ // [v1.20.3] 在线媒体嵌入播放器(YouTube/B站/抖音/QQ音乐/网易云等)
2728
+ const mediaEmbedHtml = (() => {
2729
+ const mediaList = (msg._media || []);
2730
+ if (!mediaList || mediaList.length === 0) return '';
2731
+ let parts = [];
2732
+ for (const m of mediaList) {
2733
+ const isAudio = m.media_type === 'audio';
2734
+ let embedUrl = m.embed_url || '';
2735
+ const title = m.title || (isAudio ? '在线音乐' : '在线视频');
2736
+ const origUrl = m.original_url || embedUrl;
2737
+ if (!embedUrl) continue;
2738
+ // 前端 URL → 嵌入 URL 转换(历史回放时 embed_url 可能是原始 URL)
2739
+ if (embedUrl && !embedUrl.includes('/embed/') && !embedUrl.includes('/player')) {
2740
+ const ytMatch = embedUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
2741
+ if (ytMatch) { embedUrl = 'https://www.youtube.com/embed/' + ytMatch[1]; }
2742
+ const biliMatch = embedUrl.match(/bilibili\.com\/video\/(BV[\w]+)/);
2743
+ if (biliMatch) { embedUrl = 'https://player.bilibili.com/player.html?bvid=' + biliMatch[1] + '&autoplay=0'; }
2744
+ const neteaseMatch = embedUrl.match(/music\.163\.com.*[?&]id=(\d+)/);
2745
+ if (neteaseMatch) { embedUrl = 'https://music.163.com/outchain/player?type=2&id=' + neteaseMatch[1] + '&auto=0&height=66'; }
2746
+ }
2747
+ if (isAudio) {
2748
+ parts.push('<div class="msg-media-embed msg-media-audio">' +
2749
+ '<div class="msg-media-header"><span class="msg-media-icon">🎵</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
2750
+ '<a class="msg-media-link" href="' + escapeHtml(origUrl) + '" target="_blank" rel="noopener" title="在新窗口打开">↗</a></div>' +
2751
+ '<iframe src="' + escapeHtml(embedUrl) + '" style="width:100%;max-width:480px;height:80px;border:none;border-radius:8px" loading="lazy" allow="autoplay" sandbox="allow-scripts allow-same-origin allow-presentation"></iframe>' +
2752
+ '</div>');
2753
+ } else {
2754
+ parts.push('<div class="msg-media-embed msg-media-video">' +
2755
+ '<div class="msg-media-header"><span class="msg-media-icon">🎬</span><span class="msg-media-label">' + escapeHtml(title) + '</span>' +
2756
+ '<a class="msg-media-link" href="' + escapeHtml(origUrl) + '" target="_blank" rel="noopener" title="在新窗口打开">↗</a></div>' +
2757
+ '<iframe src="' + escapeHtml(embedUrl) + '" style="width:100%;max-width:640px;height:360px;border:none;border-radius:8px" loading="lazy" allow="autoplay;encrypted-media;picture-in-picture" allowfullscreen></iframe>' +
2758
+ '</div>');
2759
+ }
2760
+ }
2761
+ return parts.length > 0 ? '<div class="msg-attachments msg-attachments-media">' + parts.join('') + '</div>' : '';
2762
+ })();
2701
2763
  const fileAttachmentHtml = (() => {
2702
2764
  let parts = [];
2703
2765
  // User files(用户发送的非图片文件)
@@ -2717,12 +2779,14 @@ function _renderMessagesInner() {
2717
2779
  '</div>');
2718
2780
  }
2719
2781
  }
2720
- // Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files(只渲染非图片文件)
2782
+ // Agent files (v2_file events) — 支持实时流式 _files 和历史加载的 files(只渲染非图片/音频/视频文件)
2721
2783
  const agentFiles = (msg._files || []);
2722
2784
  if (!isUser && agentFiles.length > 0) {
2723
2785
  for (const f of agentFiles) {
2724
2786
  const isImage = f.type && f.type.startsWith('image/');
2725
- if (isImage) continue; // 图片已在 imageAttachmentHtml 中渲染
2787
+ const isAudio = f.type && f.type.startsWith('audio/');
2788
+ const isVideo = f.type && f.type.startsWith('video/');
2789
+ if (isImage || isAudio || isVideo) continue; // 图片/音视频已在上方渲染为内嵌播放器
2726
2790
  const fileId = f.id;
2727
2791
  const sizeStr = f.size ? formatFileSize(f.size) : '';
2728
2792
  const icon = _getFileIcon(f.name || f.type || '');
@@ -2845,6 +2909,7 @@ function _renderMessagesInner() {
2845
2909
  ${taskPlanHtml}
2846
2910
  ${finishReasonHtml}
2847
2911
  ${imageAttachmentHtml}
2912
+ ${mediaEmbedHtml}
2848
2913
  ${bubbleHtml}
2849
2914
  ${fileAttachmentHtml}
2850
2915
  ${streamingIndicator}
@@ -1858,6 +1858,15 @@ async function sendMessage(opts) {
1858
1858
  state.messages[msgIdx]._files.push(evt.data);
1859
1859
  throttledStreamUpdate(msgIdx);
1860
1860
  }
1861
+ } else if (evt.type === 'v2_media') {
1862
+ // [v1.20.3] Agent is embedding a media player (audio/video)
1863
+ if (evt.data) {
1864
+ if (!state.messages[msgIdx]._media) state.messages[msgIdx]._media = [];
1865
+ state.messages[msgIdx]._media.push(evt.data);
1866
+ // 播放媒体时自动静音 TTS
1867
+ if (typeof muteTTS === 'function') muteTTS();
1868
+ throttledStreamUpdate(msgIdx);
1869
+ }
1861
1870
  } else if (evt.type === 'v2_session_rename') {
1862
1871
  // [v1.15.8] 会话自动命名 — 后端通过 mainsubject 生成
1863
1872
  if (evt.data && evt.data.name) {