myagent-ai 1.21.1 → 1.22.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.
@@ -17,6 +17,7 @@ from agents.base import BaseAgent, AgentContext
17
17
  from core.utils import generate_id, timestamp, truncate_str
18
18
  from core.context_builder import ContextBuilder
19
19
  from core.output_parser import ParsedOutput, parse_output, validate_output, extract_surrounding_text
20
+ from core.tool_dispatcher import ToolDispatcher
20
21
 
21
22
  logger = get_logger("myagent.agent.main")
22
23
 
@@ -47,7 +48,7 @@ class MainAgent(BaseAgent):
47
48
  <mainsubject>当前对话的6字以内标题(每轮都需输出,系统会每3轮自动更新会话名称)</mainsubject>
48
49
  <usersays_correct>通过修正识别错误、调整标点,结合上下文,将用户语音转写文本"usersays"修正为更准确的文本,但尽量少改动。如"usersays"为空,则此处为空。</usersays_correct>
49
50
  <response><reply>展示给用户的文本,格式上尽量使用md格式,直观形象展示,甚至可以包括超链接等。内容上,直接回应用户问题;告诉用户,为完成任务,准备如何开展工作;根据工具调用结果,展示任务进展;任务完成后的最终总结。注意:这是给用户展示信息的最重要标签,内容清晰明了,尽量不要跟上次回复重复。</reply><toolstocal>
50
- <tool><beforecalltext>展示给用户的工具调用信息,方便用户了解调用目的。格式:先使用"接下来、下一步、接着、现在、然后、最后"等连接词,然后介绍调用什么工具,达到什么目的。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>预估调用超时时限(秒),工具调用超时会立即回调大语言模型,方便调整工具使用</timeout></tool>
51
+ <tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>预估调用超时时限(秒),工具调用超时会立即回调大语言模型,方便调整工具使用</timeout></tool>
51
52
  </toolstocal>
52
53
  </response>
53
54
  <task_plan>若"context"包含非空"task_plan",则更新它:若任务条数已超8,则精简为3条,若主题发生明显变化,重新设计任务列表。若"context"包含空"task_plan",则先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过2步,则此处输出为空,不创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过2步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记。</task_plan>
@@ -71,8 +72,7 @@ class MainAgent(BaseAgent):
71
72
  - **搜索信息**: 用 `web_search`(返回标题+URL+摘要)
72
73
  - **读取网页内容**: 用 `web_read`(传入URL,提取正文)
73
74
  - **浏览器交互**(填表、点击、截图等): 才使用 browser_open / browser_click 等
74
- - **执行代码**: 用 `code` 工具(language: python/javascript/shell)
75
- - **执行命令**: 用 `command` 或 `command_run` 工具
75
+ - **执行命令/代码**: 用 `command` 工具执行 shell 命令(python/node/bash 等代码也通过 command 执行,如 `python script.py`、`node app.js`)
76
76
  - **文件操作**: 用 `file_read` / `file_write` / `file_list` 等文件工具
77
77
  - **发送文件给用户**: 用 `file_send` 工具(参数: file_path=文件路径, description=说明),当你生成或处理了文件需要返回给用户时使用
78
78
  - **播放音频**: 用 `playaudio` 工具(参数: url=音乐链接或file_path=本地文件路径),在聊天中内嵌播放音频(支持QQ音乐、YouTube音乐、本地MP3/WAV等),播放时自动关闭语音合成
@@ -90,12 +90,16 @@ class MainAgent(BaseAgent):
90
90
  - 关闭: `{"action": "close", "session_id": "xxx"}`
91
91
  - 注意: 网页通过服务端代理加载,用户可在面板中手动操作。复杂交互建议先用 get_content 查看页面结构再用 click/fill。
92
92
  - **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
93
+ - **OCR 文字识别**: 用 `image_ocr` 工具(参数: image_path=图片路径, lang=ch/en),从截图、扫描件、照片中提取文字
94
+ - **图片内容分析**: 用 `image_analyze` 工具(参数: image_path=图片路径, prompt=分析提示词),识别图片中的物体、文字、图表数据等(需模型支持视觉)
95
+ - **语音转文字**: 用 `audio_transcribe` 工具(参数: audio_path=音频路径, language=zh/en),将音频文件转录为文本
96
+ - **专业技能指令**: 系统内置了丰富的专业技能指南(如 PDF/DOCX/XLSX/PPT 生成、图表绘制、前端开发、全栈开发、图像生成等),当你需要执行特定领域的复杂任务时,通过 `<get_knowledge>` 标签请求相关技能指令(如填写 "PDF文档生成指南"、"PPT制作规范" 等),系统将在下一轮通过 `<knowledge>` 注入完整指令。
93
97
  4. 准备好内容后,最后,再检查输出格式,确保满足以下要求:
94
98
  <output>
95
99
  <mainsubject>当前对话的6字以内标题(每轮都需输出,系统会每3轮自动更新会话名称)</mainsubject>
96
100
  <usersays_correct>通过修正识别错误、调整标点,结合上下文,将用户语音转写文本"usersays"修正为更准确的文本,但尽量少改动。如"usersays"为空,则此处为空。</usersays_correct>
97
101
  <response><reply>展示给用户的文本,格式上尽量使用md格式,直观形象展示,甚至可以包括超链接等。内容上,直接回应用户问题;告诉用户,为完成任务,准备如何开展工作;根据工具调用结果,展示任务进展;任务完成后的最终总结。注意:这是给用户展示信息的最重要标签,内容清晰明了,尽量不要跟上次回复重复。</reply><toolstocal>
98
- <tool><beforecalltext>展示给用户的工具调用信息,方便用户了解调用目的。格式:先使用"接下来、下一步、接着、现在、然后、最后"等连接词,然后介绍调用什么工具,达到什么目的。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>预估调用超时时限(秒),工具调用超时会立即回调大语言模型,方便调整工具使用</timeout></tool>
102
+ <tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>预估调用超时时限(秒),工具调用超时会立即回调大语言模型,方便调整工具使用</timeout></tool>
99
103
  </toolstocal>
100
104
  </response>
101
105
  <task_plan>若"context"包含非空"task_plan",则更新它:若任务条数已超8,则精简为3条,若主题发生明显变化,重新设计任务列表。若"context"包含空"task_plan",则先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过2步,则此处输出为空,不创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过2步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记。</task_plan>
@@ -117,6 +121,8 @@ class MainAgent(BaseAgent):
117
121
  self._iteration_count = 0
118
122
  self._current_task_id: str = ""
119
123
  self._registered_task: bool = False
124
+ # [v1.22.0] 统一工具分发器
125
+ self.dispatcher: Optional[ToolDispatcher] = None
120
126
  # Context Builder (结构化上下文构建)
121
127
  self.context_builder: Optional[ContextBuilder] = None
122
128
  # 执行事件追踪(用于前端展示命令执行过程)
@@ -1528,9 +1534,7 @@ class MainAgent(BaseAgent):
1528
1534
  stream_callback: Optional[Callable] = None,
1529
1535
  sent_files: Optional[List[Dict[str, Any]]] = None,
1530
1536
  ) -> Dict[str, Any]:
1531
- """V2 工具执行"""
1532
- result = {"success": False, "output": "", "error": ""}
1533
-
1537
+ """[v1.22.0] V2 工具执行 — 统一分发到 ToolDispatcher"""
1534
1538
  try:
1535
1539
  import json as _json
1536
1540
  try:
@@ -1538,355 +1542,19 @@ class MainAgent(BaseAgent):
1538
1542
  except (_json.JSONDecodeError, TypeError):
1539
1543
  params = {"raw_input": parms_str}
1540
1544
 
1541
- if tool_name == "code" or tool_name.startswith("code_"):
1542
- code_lang = params.get("language", "python")
1543
- code_text = params.get("code", parms_str)
1544
- if self.executor:
1545
- # 注入权限检查器(V1 路径在 api_server 中设置,V2 路径需要在此设置)
1546
- self.executor.set_permission_checker(
1547
- self.check_permission, self.name
1548
- )
1549
- exec_result = await self.executor.execute(
1550
- language=code_lang,
1551
- code=code_text,
1552
- timeout=timeout,
1553
- )
1554
- result = exec_result.to_dict()
1555
- else:
1556
- result["error"] = "执行引擎未初始化"
1557
-
1558
- elif tool_name == "command" or tool_name == "command_run":
1559
- code_text = params.get("command", parms_str)
1560
- if self.executor:
1561
- # 注入权限检查器(V1 路径在 api_server 中设置,V2 路径需要在此设置)
1562
- self.executor.set_permission_checker(
1563
- self.check_permission, self.name
1564
- )
1565
- exec_result = await self.executor.execute(
1566
- language="shell",
1567
- code=code_text,
1568
- timeout=timeout,
1569
- )
1570
- result = exec_result.to_dict()
1571
- else:
1572
- result["error"] = "执行引擎未初始化"
1573
-
1574
- elif tool_name == "recall_memory":
1575
- # === 主动召回记忆工具 ===
1576
- # 根据 memory_agent.recall_memory() 搜索历史记忆
1577
- try:
1578
- if self.memory_agent:
1579
- recall_results = await self.memory_agent.recall_memory(
1580
- keyword=params.get("keyword", ""),
1581
- time_point=params.get("time_point", ""),
1582
- session_id=params.get("session_id", ""),
1583
- limit=params.get("limit", 5),
1584
- )
1585
- if recall_results:
1586
- output_lines = [f"找到 {len(recall_results)} 条相关记忆:"]
1587
- for i, mem in enumerate(recall_results, 1):
1588
- output_lines.append(
1589
- f"{i}. [{mem.get('created_at', '')}] "
1590
- f"[{mem.get('category', '')}] "
1591
- f"{mem.get('content', '')}"
1592
- )
1593
- result = {"success": True, "output": "\n".join(output_lines), "data": recall_results}
1594
- else:
1595
- result = {"success": True, "output": "未找到相关记忆", "data": []}
1596
- else:
1597
- result = {"success": False, "error": "记忆系统未初始化"}
1598
- except Exception as re_err:
1599
- result = {"success": False, "error": f"记忆召回失败: {re_err}"}
1600
- logger.warning(f"[{task_id}] recall_memory 工具异常: {re_err}")
1601
-
1602
- elif tool_name == "file_send":
1603
- # [v1.16.17→18] 文件发送工具 — 让 Agent 向用户发送文件
1604
- try:
1605
- from skills.file_send import FileSendSkill
1606
- _fskill = FileSendSkill()
1607
- _fpath = params.get("file_path", "")
1608
- _fdesc = params.get("description", "")
1609
- # [v1.16.18] 使用当前作用域的 stream_callback(而非 context._stream_callback)
1610
- _fresult = await _fskill.execute(_fpath, _fdesc, stream_callback=stream_callback)
1611
- result = {"success": True, "output": json.dumps(_fresult, ensure_ascii=False, indent=2), "data": _fresult}
1612
- # [v1.18.5→21.1] 追踪发送的文件,用于持久化到会话记忆
1613
- if sent_files is not None and _fresult.get("success") and _fresult.get("file_id"):
1614
- sent_files.append({
1615
- "id": _fresult["file_id"],
1616
- "name": _fresult.get("name", ""),
1617
- "type": _fresult.get("type", ""),
1618
- "size": _fresult.get("size", 0),
1619
- })
1620
- except Exception as _fse:
1621
- result = {"success": False, "error": f"文件发送失败: {_fse}"}
1622
- logger.warning(f"[{task_id}] file_send 工具异常: {_fse}")
1623
-
1624
- elif tool_name in ("playaudio", "playvideo"):
1625
- # [v1.20.3] 音视频播放工具 — 在聊天中内嵌播放器
1626
- try:
1627
- _media_url = params.get("url", "").strip()
1628
- _media_file = params.get("file_path", "").strip()
1629
- _media_type = "audio" if tool_name == "playaudio" else "video"
1630
- _embed_url = None
1631
- _embed_title = params.get("title", "")
1632
- _fallback_link = None # [v1.20.10] 无法嵌入时提供外部链接
1633
-
1634
- if _media_url:
1635
- # 在线链接 — 提取嵌入式播放 URL
1636
- import re
1637
- _url_lower = _media_url.lower()
1638
- # YouTube: https://www.youtube.com/watch?v=xxx 或 youtu.be/xxx
1639
- _yt_match = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([\w-]+)', _media_url)
1640
- if _yt_match:
1641
- _embed_url = f"https://www.youtube.com/embed/{_yt_match.group(1)}"
1642
- _embed_title = _embed_title or "YouTube 视频"
1643
- # YouTube Music 播放列表: https://music.youtube.com/playlist?list=xxx
1644
- elif 'music.youtube.com' in _url_lower:
1645
- _ym_match = re.search(r'list=([\w-]+)', _media_url)
1646
- if _ym_match:
1647
- _embed_url = f"https://music.youtube.com/embed?list={_ym_match.group(1)}&layout=full"
1648
- _embed_title = _embed_title or "YouTube Music"
1649
- else:
1650
- # 单曲: https://music.youtube.com/watch?v=xxx
1651
- _ymv_match = re.search(r'watch\?v=([\w-]+)', _media_url)
1652
- if _ymv_match:
1653
- _embed_url = f"https://music.youtube.com/embed/{_ymv_match.group(1)}"
1654
- _embed_title = _embed_title or "YouTube Music"
1655
- else:
1656
- _fallback_link = _media_url
1657
- _embed_title = _embed_title or "YouTube Music"
1658
- # Bilibili: https://www.bilibili.com/video/BVxxx 或 b23.tv/xxx
1659
- elif 'bilibili.com' in _url_lower or 'b23.tv' in _url_lower:
1660
- _bv_match = re.search(r'bilibili\.com/video/(BV[\w]+)', _media_url)
1661
- if _bv_match:
1662
- _embed_url = f"https://player.bilibili.com/player.html?bvid={_bv_match.group(1)}&autoplay=0"
1663
- _embed_title = _embed_title or "B站视频"
1664
- else:
1665
- _embed_url = _media_url # b23.tv 短链接直接使用
1666
- _embed_title = _embed_title or "B站视频"
1667
- # QQ音乐: https://y.qq.com/n/ryqq/songDetail/xxx
1668
- elif 'y.qq.com' in _url_lower:
1669
- # QQ音乐支持 outchain player
1670
- _qq_match = re.search(r'songDetail/(\w+)', _media_url)
1671
- if _qq_match:
1672
- _embed_url = f"https://y.qq.com/n/ryqq/songDetail/{_qq_match.group(1)}"
1673
- _embed_title = _embed_title or "QQ音乐"
1674
- else:
1675
- _embed_url = _media_url
1676
- _embed_title = _embed_title or "QQ音乐"
1677
- # 网易云音乐: https://music.163.com/song?id=xxx
1678
- elif 'music.163.com' in _url_lower:
1679
- _song_match = re.search(r'music\.163\.com.*[?&]id=(\d+)', _media_url)
1680
- if _song_match:
1681
- _embed_url = f"https://music.163.com/outchain/player?type=2&id={_song_match.group(1)}&auto=0&height=66"
1682
- _embed_title = _embed_title or "网易云音乐"
1683
- else:
1684
- _embed_url = _media_url
1685
- _embed_title = _embed_title or "网易云音乐"
1686
- # 抖音: https://www.douyin.com/video/xxx
1687
- elif 'douyin.com' in _url_lower:
1688
- _embed_url = _media_url
1689
- _embed_title = _embed_title or "抖音视频"
1690
- else:
1691
- # 其他 URL,尝试直接嵌入
1692
- _embed_url = _media_url
1693
- _embed_title = _embed_title or ("在线音乐" if _media_type == "audio" else "在线视频")
1694
-
1695
- if _embed_url and stream_callback:
1696
- # 在线播放 — 发送 v2_media 事件让前端渲染嵌入播放器
1697
- await _safe_sse(stream_callback, {
1698
- "type": "v2_media",
1699
- "data": {
1700
- "media_type": _media_type,
1701
- "embed_url": _embed_url,
1702
- "title": _embed_title,
1703
- "original_url": _media_url,
1704
- }
1705
- })
1706
- result = {"success": True, "output": f"已嵌入{_embed_title}播放器: {_media_url}"}
1707
-
1708
- elif _fallback_link and stream_callback:
1709
- # [v1.20.10] 无法嵌入但提供外部链接 — 发送链接卡片
1710
- await _safe_sse(stream_callback, {
1711
- "type": "v2_media",
1712
- "data": {
1713
- "media_type": _media_type,
1714
- "embed_url": "", # 空 embed_url 告诉前端渲染链接
1715
- "title": _embed_title,
1716
- "original_url": _fallback_link,
1717
- }
1718
- })
1719
- result = {"success": True, "output": f"已提供{_embed_title}链接(不支持嵌入播放): {_fallback_link}"}
1720
-
1721
- elif _media_file:
1722
- # 本地文件 — 使用 file_send 发送文件,前端渲染内嵌播放器
1723
- from pathlib import Path as _P
1724
- _fpath = _P(_media_file).expanduser().resolve()
1725
- if not _fpath.exists():
1726
- result = {"success": False, "error": f"文件不存在: {_media_file}"}
1727
- else:
1728
- from skills.file_send import FileSendSkill
1729
- _fskill = FileSendSkill()
1730
- _desc = f"{'音频' if _media_type == 'audio' else '视频'}播放: {_fpath.name}"
1731
- _fresult = await _fskill.execute(str(_fpath), _desc, stream_callback=stream_callback)
1732
- if _fresult.get("success"):
1733
- # 标记为媒体文件,前端渲染内嵌播放器
1734
- _fresult["_media_type"] = _media_type
1735
- result = {"success": True, "output": f"已发送{_media_type}文件: {_fpath.name}", "data": _fresult}
1736
- if sent_files is not None and _fresult.get("file_id"):
1737
- sent_files.append({
1738
- "id": _fresult["file_id"],
1739
- "name": _fresult.get("name", ""),
1740
- "type": _fresult.get("type", ""),
1741
- "size": _fresult.get("size", 0),
1742
- "_media_type": _media_type,
1743
- })
1744
- else:
1745
- result = {"success": False, "error": _fresult.get("error", "文件发送失败")}
1746
- else:
1747
- result = {"success": False, "error": f"请提供 url(在线链接)或 file_path(本地文件路径)参数"}
1748
-
1749
- except Exception as _me:
1750
- result = {"success": False, "error": f"播放工具异常: {_me}"}
1751
- logger.warning(f"[{task_id}] {tool_name} 工具异常: {_me}")
1752
-
1753
- elif tool_name == "web_control":
1754
- # [v1.21.0] 网页控制器 — 在聊天中打开可控制的浏览器面板
1755
- try:
1756
- from core.web_control import get_web_control_manager
1757
- _wc_mgr = get_web_control_manager()
1758
- _wc_action = params.get("action", "open")
1759
- _wc_session_id = params.get("session_id", "").strip()
1760
-
1761
- # 自动获取或创建会话
1762
- _wc_session = None
1763
- if _wc_session_id:
1764
- _wc_session = _wc_mgr.get_session(_wc_session_id)
1765
- if not _wc_session:
1766
- _wc_session = _wc_mgr.create_session()
1767
- _wc_session_id = _wc_session.session_id
1768
-
1769
- if _wc_action == "open":
1770
- # 打开面板 — 发送 SSE 事件让前端弹出控制面板
1771
- _wc_url = params.get("url", "").strip()
1772
- if _wc_url:
1773
- _wc_session.current_url = _wc_url
1774
- if stream_callback:
1775
- await _safe_sse(stream_callback, {
1776
- "type": "v2_web_control",
1777
- "data": {
1778
- "action": "open",
1779
- "session_id": _wc_session_id,
1780
- "url": _wc_url,
1781
- "panel_url": f"/api/web_control/panel?sid={_wc_session_id}",
1782
- }
1783
- })
1784
- result = {
1785
- "success": True,
1786
- "output": f"已打开网页控制面板 (session: {_wc_session_id})" + (f",URL: {_wc_url}" if _wc_url else ""),
1787
- "session_id": _wc_session_id,
1788
- }
1789
-
1790
- elif _wc_action == "close":
1791
- _wc_mgr.close_session(_wc_session_id)
1792
- if stream_callback:
1793
- await _safe_sse(stream_callback, {
1794
- "type": "v2_web_control",
1795
- "data": {"action": "close", "session_id": _wc_session_id}
1796
- })
1797
- result = {"success": True, "output": f"已关闭网页控制面板 (session: {_wc_session_id})"}
1798
-
1799
- elif _wc_action == "navigate":
1800
- _wc_url = params.get("url", "").strip()
1801
- if not _wc_url:
1802
- result = {"success": False, "error": "请提供 url 参数"}
1803
- else:
1804
- _wc_session.current_url = _wc_url
1805
- if stream_callback:
1806
- await _safe_sse(stream_callback, {
1807
- "type": "v2_web_control",
1808
- "data": {"action": "navigate", "session_id": _wc_session_id, "url": _wc_url}
1809
- })
1810
- result = {"success": True, "output": f"正在导航到: {_wc_url}"}
1811
-
1812
- elif _wc_action in ("set_cookies", "get_cookies"):
1813
- # Cookie 操作 — 服务端直接处理
1814
- if _wc_action == "set_cookies":
1815
- _cookies = params.get("cookies", [])
1816
- if isinstance(_cookies, str):
1817
- import json as _jc
1818
- try:
1819
- _cookies = _jc.loads(_cookies)
1820
- except:
1821
- _cookies = []
1822
- for _c in _cookies:
1823
- if isinstance(_c, dict):
1824
- _c_domain = _c.get("domain", "").lstrip(".")
1825
- _c_name = _c.get("name", "")
1826
- _c_value = _c.get("value", "")
1827
- if _c_domain and _c_name:
1828
- _wc_session.cookies[f"{_c_domain}::{_c_name}"] = _c_value
1829
- result = {"success": True, "output": f"已设置 {len(_cookies)} 个 cookie", "total_cookies": len(_wc_session.cookies)}
1830
- else:
1831
- _cookie_list = []
1832
- for _ck, _cv in _wc_session.cookies.items():
1833
- _parts = _ck.split("::", 1)
1834
- if len(_parts) == 2:
1835
- _cookie_list.append({"domain": _parts[0], "name": _parts[1], "value": _cv})
1836
- result = {"success": True, "output": json.dumps(_cookie_list, ensure_ascii=False), "cookies": _cookie_list}
1837
-
1838
- else:
1839
- # 其他操作(click, fill, scroll, evaluate, get_content, wait, screenshot)
1840
- # 通过命令队列下发到客户端执行,阻塞等待结果
1841
- _wc_params = {k: v for k, v in params.items() if k not in ("action", "session_id")}
1842
- _wc_timeout = int(params.get("timeout", timeout))
1843
- _wc_result = await _wc_mgr.queue_command(
1844
- session_id=_wc_session_id,
1845
- action=_wc_action,
1846
- params=_wc_params,
1847
- timeout=min(_wc_timeout, 60), # 最大 60 秒
1848
- )
1849
- result = _wc_result
1850
-
1851
- except Exception as _wce:
1852
- result = {"success": False, "error": f"网页控制器异常: {_wce}"}
1853
- logger.warning(f"[{task_id}] web_control 工具异常: {_wce}")
1854
-
1855
- elif self.skills:
1856
- exec_result = await self.skills.execute(tool_name, **params)
1857
- if exec_result is None:
1858
- result["error"] = f"技能 {tool_name} 返回了空结果"
1859
- else:
1860
- result = exec_result.to_dict()
1861
- # [v1.21.1] Skill 生成文件后自动通过 file_send 发送给前端
1862
- if exec_result.success and exec_result.files:
1863
- try:
1864
- from skills.file_send import FileSendSkill
1865
- _auto_fsend = FileSendSkill()
1866
- for _auto_fpath in exec_result.files:
1867
- if _auto_fpath and os.path.isfile(_auto_fpath):
1868
- _auto_fres = await _auto_fsend.execute(
1869
- _auto_fpath, stream_callback=stream_callback)
1870
- if _auto_fres.get("success") and _auto_fres.get("file_id"):
1871
- # 记录发送的 file_id
1872
- if not result.get("_sent_file_ids"):
1873
- result["_sent_file_ids"] = []
1874
- result["_sent_file_ids"].append(_auto_fres["file_id"])
1875
- # 追踪到 sent_files(持久化到会话记忆)
1876
- if sent_files is not None:
1877
- sent_files.append({
1878
- "id": _auto_fres["file_id"],
1879
- "name": _auto_fres.get("name", ""),
1880
- "type": _auto_fres.get("type", ""),
1881
- "size": _auto_fres.get("size", 0),
1882
- })
1883
- except Exception as _afe:
1884
- logger.warning(f"[{task_id}] 自动 file_send 失败: {_afe}")
1885
- else:
1886
- result["error"] = f"未知工具: {tool_name}"
1545
+ if self.dispatcher:
1546
+ return await self.dispatcher.dispatch(
1547
+ tool_name=tool_name,
1548
+ params=params,
1549
+ timeout=timeout,
1550
+ task_id=task_id,
1551
+ stream_callback=stream_callback,
1552
+ sent_files=sent_files,
1553
+ )
1887
1554
 
1555
+ # 兼容回退: dispatcher 未初始化时使用基础 fallback
1556
+ result = {"success": False, "output": "", "error": f"工具分发器未初始化: {tool_name}"}
1888
1557
  except Exception as e:
1889
- result["error"] = str(e)
1890
- logger.error(f"[{task_id}] 工具 {tool_name} 异常: {e}")
1891
-
1558
+ result = {"success": False, "output": "", "error": f"工具调用异常: {tool_name} - {e}"}
1559
+ logger.warning(f"[{task_id}] 工具调用异常 ({tool_name}): {e}")
1892
1560
  return result
@@ -92,22 +92,9 @@ class WhatsAppBot(BaseChatBot):
92
92
  # 检查是否已安装依赖
93
93
  node_modules = bridge_dir / "node_modules"
94
94
  if not node_modules.exists():
95
- self.logger.info("正在安装 Baileys 依赖...")
96
- try:
97
- proc = await asyncio.create_subprocess_exec(
98
- "npm", "install", "--production",
99
- cwd=str(bridge_dir),
100
- stdout=asyncio.subprocess.PIPE,
101
- stderr=asyncio.subprocess.PIPE,
102
- )
103
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
104
- if proc.returncode != 0:
105
- self.logger.error(f"Baileys 安装失败: {stderr.decode()[:500]}")
106
- return
107
- self.logger.info("Baileys 依赖安装完成")
108
- except Exception as e:
109
- self.logger.error(f"Baileys 安装异常: {e}")
110
- return
95
+ self.logger.warning("Baileys 依赖未安装,请手动执行:")
96
+ self.logger.warning(f" cd {bridge_dir} && npm install")
97
+ return
111
98
 
112
99
  env = os.environ.copy()
113
100
  env["SESSION_DIR"] = self._session_dir
@@ -790,45 +790,14 @@ class ContextBuilder:
790
790
  skill_registry: Optional["SkillRegistry"],
791
791
  ) -> str:
792
792
  """
793
- 构建 <skill_prompts> 段落 —— SKILL.md 格式技能的完整指令内容。
793
+ [v1.22.0] 不再全量注入 skill_prompts
794
794
 
795
- 对于 SKILL.md 目录格式的 Prompt 类型技能,将完整的 markdown body
796
- 注入到 LLM 上下文中,使 LLM 能按照技能指令执行任务。
797
- 普通 Python 技能(有 execute 实现)不在此处注入。
798
-
799
- Returns:
800
- <skill_prompts> XML 段落字符串,无 Prompt 技能时返回空字符串
795
+ SKILL.md 内容已通过 _sync_skill_guides_to_knowledge() 写入
796
+ {kb_dir}/_skill_guides/ 目录,由 RAG 索引。
797
+ LLM 需要专业技能指令时,通过 <get_knowledge> 按需检索。
798
+ 返回空字符串以节省大量 token。
801
799
  """
802
- if not skill_registry:
803
- return ""
804
-
805
- # 收集所有 markdown 类型且未禁用的技能的 body
806
- prompt_skills = []
807
- try:
808
- for skill in skill_registry._skills.values():
809
- if skill_registry._is_disabled(skill.name):
810
- continue
811
- # 检查是否为 SKILL.md 格式技能
812
- body = getattr(skill, 'get_body', None)
813
- if body and callable(body):
814
- content = body()
815
- if content:
816
- prompt_skills.append((skill.name, content))
817
- except Exception as e:
818
- logger.warning(f"获取 skill prompts 失败: {e}")
819
- return ""
820
-
821
- if not prompt_skills:
822
- return ""
823
-
824
- lines: List[str] = ["<skill_prompts>"]
825
- for skill_name, content in prompt_skills:
826
- safe_name = _xml_escape(skill_name)
827
- lines.append(f"<{safe_name}>")
828
- lines.append(content)
829
- lines.append(f"</{safe_name}>")
830
- lines.append("</skill_prompts>")
831
- return "\n".join(lines)
800
+ return ""
832
801
 
833
802
  # =========================================================================
834
803
  # Token 预算管理