myagent-ai 1.21.1 → 1.22.0
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/agents/main_agent.py +24 -356
- package/core/context_builder.py +6 -37
- package/core/stt.py +283 -0
- package/core/tool_dispatcher.py +557 -0
- package/main.py +104 -11
- package/package.json +1 -1
- package/requirements-optional.txt +7 -1
- package/skills/registry.py +171 -0
- package/web/api_server.py +53 -5
- package/web/ui/index.html +101 -18
package/agents/main_agent.py
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
|
1890
|
-
logger.
|
|
1891
|
-
|
|
1558
|
+
result = {"success": False, "output": "", "error": f"工具调用异常: {tool_name} - {e}"}
|
|
1559
|
+
logger.warning(f"[{task_id}] 工具调用异常 ({tool_name}): {e}")
|
|
1892
1560
|
return result
|
package/core/context_builder.py
CHANGED
|
@@ -790,45 +790,14 @@ class ContextBuilder:
|
|
|
790
790
|
skill_registry: Optional["SkillRegistry"],
|
|
791
791
|
) -> str:
|
|
792
792
|
"""
|
|
793
|
-
|
|
793
|
+
[v1.22.0] 不再全量注入 skill_prompts。
|
|
794
794
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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 预算管理
|