feat(search): 实现 SearchToolDispatcher 工具调度器 (Task 7)

- 新增 app/search/tools.py:
  - get_tool_descriptions() 工具描述文本(与 TRM4 一致)
  - SearchToolDispatcher 类实现 ToolDispatcher Protocol
  - dispatch() 按工具名路由: view_node / search_similar /
    observe_frame / submit_answer / read_skill
  - ValueError(未知工具)上抛,KeyError/FileNotFoundError 捕获返回错误文本
  - view_node: env.get_node_text + summarize_node + get_children_info + summarize_children
  - search_similar: env.search_similar + summarize_nodes_batch
  - observe_frame: env.resolve_frame_paths + get_subtitle + observe_frame + 字幕前置

- 修复 app/tree/environment.py get_children_info():
  - 原实现返回 _format_time_range (str) 导致 summarize_children 解包失败
  - 改为返回原始数值元组 via 新增 _node_time_range_raw 静态方法

- 新增 tests/unit/test_search_tools.py (14 tests):
  - get_tool_descriptions 含/不含 read_skill
  - 五种工具 dispatch 路由验证
  - 未知工具 ValueError + 节点不存在错误文本

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 06:07:27 -04:00
parent ca3ea1cdf2
commit f4f92b0938
3 changed files with 788 additions and 1 deletions
+21 -1
View File
@@ -339,6 +339,7 @@ class TreeEnvironment:
返回:
子节点信息列表,每项包含 {"id", "time_range", "summary"}。
time_range 为 (start, end) 数值元组(L3 节点退化为 (ts, ts))。
L3 叶子节点返回空列表。
异常:
@@ -357,7 +358,7 @@ class TreeEnvironment:
result.append(
{
"id": child.id,
"time_range": self._format_time_range(child),
"time_range": self._node_time_range_raw(child),
"summary": desc,
}
)
@@ -504,6 +505,25 @@ class TreeEnvironment:
return f"{node.timestamp:.1f}s"
return "N/A"
@staticmethod
def _node_time_range_raw(node: AnyNode) -> tuple[float, float]:
"""提取节点时间范围的原始数值元组。
L1/L2 返回 time_range 元组;L3 退化为 (timestamp, timestamp)
全部为 None 时兜底 (0.0, 0.0)。
参数:
node: 树节点。
返回:
(start, end) 秒级数值元组。
"""
if isinstance(node, (L1Node, L2Node)) and node.time_range:
return node.time_range
if isinstance(node, L3Node) and node.timestamp is not None:
return (node.timestamp, node.timestamp)
return (0.0, 0.0)
@staticmethod
def _get_children(node: AnyNode) -> list[AnyNode]:
"""获取节点的直接子节点列表。