feat(tools): 新增 4 个 agent 工具 —— web_search / web_fetch / calculator / current_datetime

均在 mcp-go,注册即 agent 可见(dispatcher 经 list_tools 动态发现,零改调度代码):

- web_search 联网搜索:Tavily(有 TAVILY_API_KEY 则用,干净 JSON)/ DuckDuckGo HTML(免 key 兜底)。
- web_fetch 网页抓取:取 URL → 去脚本样式 → 去标签 → 解实体 → 收敛空白,限 8000 rune;
  复用 external_api 的 SSRF 防护(拒环回/内网/元数据 + 重定向校验)。
- calculator 计算器:自研调度场算法求值(+ - * / % ^ 括号、一元负号),杜绝任意代码执行。
- current_datetime 当前时间:含星期与时区(可传 tz)。

测试:单测覆盖 evalArith(含右结合/一元负号/错误用例)、htmlToText(剔脚本样式+解实体)、
ddgUnwrap。live 自主 agent 实测:calculator (123+456)*7→4053、current_datetime 周三、
web_fetch example.com→200、web_search DDG 返回真实结果,全部端到端通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-24 16:47:15 +08:00
parent 700845d64a
commit e503965a83
3 changed files with 496 additions and 5 deletions
@@ -0,0 +1,58 @@
package mcp
import (
"math"
"strings"
"testing"
)
func TestEvalArith(t *testing.T) {
cases := map[string]float64{
"1+2*3": 7,
"(1+2)*3": 9,
"2^3^2": 512, // 右结合
"-3+5": 2,
"10/4": 2.5,
"10%3": 1,
"2*(3+4)-5": 9,
"-(2+3)*2": -10,
"3.5e1+0.5": 35.5,
}
for in, want := range cases {
got, err := evalArith(in)
if err != nil {
t.Errorf("evalArith(%q) 报错: %v", in, err)
continue
}
if math.Abs(got-want) > 1e-9 {
t.Errorf("evalArith(%q)=%v want %v", in, got, want)
}
}
}
func TestEvalArithErrors(t *testing.T) {
for _, in := range []string{"1/0", "1+", "(1+2", "1+2)", "rm -rf /", "import os", ""} {
if _, err := evalArith(in); err == nil {
t.Errorf("evalArith(%q) 应报错但没有", in)
}
}
}
func TestHTMLToText(t *testing.T) {
in := `<html><head><title>x</title></head><body><script>alert(1)</script>
<h1>标题</h1><p>第一段&amp;符号</p><style>.a{}</style><div>第二段</div></body></html>`
got := htmlToText(in)
if strings.Contains(got, "alert") || strings.Contains(got, ".a{}") {
t.Fatalf("脚本/样式未剔除: %q", got)
}
if !strings.Contains(got, "标题") || !strings.Contains(got, "第一段&符号") || !strings.Contains(got, "第二段") {
t.Fatalf("正文/实体提取不正确: %q", got)
}
}
func TestDDGUnwrap(t *testing.T) {
got := ddgUnwrap("//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fa&rut=x")
if got != "https://example.com/a" {
t.Fatalf("ddgUnwrap 解码错误: %q", got)
}
}