feat: 第四轮功能增强 - LLM 思维记忆优化、DevTools 记忆UI、9个新工具、5分钟自我思考

- 优化 LLM 思维方式和记忆方法(类别/重要性/关键词/相似度合并/衰减)
- DevTools 记忆查询 UI 重新设计(类别筛选/排序/星标/搜索)
- 新增 9 个 LLM 工具:calculator, datetime, file_ops, http_request, json_ops, text, random, crypto, markdown
- 管理员主对话 5 分钟自我思考增强(工具调用/记忆提取/记忆维护)
This commit is contained in:
2026-05-18 12:13:49 +08:00
parent 07781eda0e
commit b6ec36886c
20 changed files with 4654 additions and 320 deletions
@@ -0,0 +1,359 @@
package tools
import (
"context"
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// CalculatorTool performs safe mathematical expression evaluation.
// LLMs are not reliable at precise arithmetic; this tool handles complex calculations.
type CalculatorTool struct{}
// NewCalculatorTool creates a calculator tool.
func NewCalculatorTool() *CalculatorTool {
return &CalculatorTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *CalculatorTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "calculator",
Description: "执行数学计算。用于精确计算数学表达式,支持四则运算、三角函数、对数、幂运算等。适用于LLM不擅长的复杂计算场景。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"expression": map[string]interface{}{
"type": "string",
"description": "数学表达式,如 \"2 + 3 * 4\"、\"sqrt(16) + sin(pi/2)\"。支持运算符: + - * / % ^。支持函数: sqrt, sin, cos, tan, abs, floor, ceil, round, log, ln, pow。支持常量: pi, e。",
},
},
"required": []string{"expression"},
},
}
}
// Execute evaluates a mathematical expression.
func (t *CalculatorTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
expression, ok := arguments["expression"].(string)
if !ok || strings.TrimSpace(expression) == "" {
return &ToolResult{
ToolName: "calculator",
Success: false,
Error: "缺少 expression 参数",
}, nil
}
result, err := evaluate(expression)
if err != nil {
return &ToolResult{
ToolName: "calculator",
Success: false,
Error: fmt.Sprintf("计算错误: %v", err),
}, nil
}
return &ToolResult{
ToolName: "calculator",
Success: true,
Data: fmt.Sprintf("表达式: %s\n结果: %s", expression, formatResult(result)),
}, nil
}
// formatResult formats a float64 result nicely.
func formatResult(v float64) string {
if v == math.Trunc(v) && math.Abs(v) < 1e15 {
return strconv.FormatInt(int64(v), 10)
}
return strconv.FormatFloat(v, 'g', -1, 64)
}
// token types for the expression lexer.
type tokenKind int
const (
tokNumber tokenKind = iota
tokIdent
tokOp
tokLParen
tokRParen
tokComma
tokEOF
)
type token struct {
kind tokenKind
value string
}
// lexer tokenizes a mathematical expression.
type lexer struct {
input []rune
pos int
}
func newLexer(s string) *lexer {
return &lexer{input: []rune(s), pos: 0}
}
func (l *lexer) next() token {
l.skipWhitespace()
if l.pos >= len(l.input) {
return token{kind: tokEOF}
}
ch := l.input[l.pos]
// numbers (including decimals)
if unicode.IsDigit(ch) || ch == '.' {
start := l.pos
hasDot := ch == '.'
l.pos++
for l.pos < len(l.input) && (unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '.') {
if l.input[l.pos] == '.' {
if hasDot {
break
}
hasDot = true
}
l.pos++
}
return token{kind: tokNumber, value: string(l.input[start:l.pos])}
}
// identifiers (function names and constants)
if unicode.IsLetter(ch) || ch == '_' {
start := l.pos
l.pos++
for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '_') {
l.pos++
}
return token{kind: tokIdent, value: string(l.input[start:l.pos])}
}
// operators and parens
switch ch {
case '+', '-', '*', '/', '%', '^':
l.pos++
return token{kind: tokOp, value: string(ch)}
case '(':
l.pos++
return token{kind: tokLParen}
case ')':
l.pos++
return token{kind: tokRParen}
case ',':
l.pos++
return token{kind: tokComma}
}
return token{kind: tokEOF}
}
func (l *lexer) skipWhitespace() {
for l.pos < len(l.input) && unicode.IsSpace(l.input[l.pos]) {
l.pos++
}
}
// Parser evaluates expressions using recursive descent.
type parser struct {
lex *lexer
cur token
peek token
}
func newParser(lex *lexer) *parser {
p := &parser{lex: lex}
p.cur = lex.next()
p.peek = lex.next()
return p
}
func (p *parser) advance() {
p.cur = p.peek
p.peek = p.lex.next()
}
// evaluate is the entry point for expression evaluation.
func evaluate(expr string) (float64, error) {
lex := newLexer(expr)
par := newParser(lex)
result, err := par.parseExpression()
if err != nil {
return 0, err
}
if par.cur.kind != tokEOF {
return 0, fmt.Errorf("表达式末尾存在意外字符")
}
return result, nil
}
// parseExpression handles addition and subtraction.
func (p *parser) parseExpression() (float64, error) {
left, err := p.parseTerm()
if err != nil {
return 0, err
}
for p.cur.kind == tokOp && (p.cur.value == "+" || p.cur.value == "-") {
op := p.cur.value
p.advance()
right, err := p.parseTerm()
if err != nil {
return 0, err
}
if op == "+" {
left += right
} else {
left -= right
}
}
return left, nil
}
// parseTerm handles multiplication, division, modulo, and power.
func (p *parser) parseTerm() (float64, error) {
left, err := p.parseUnary()
if err != nil {
return 0, err
}
for p.cur.kind == tokOp && (p.cur.value == "*" || p.cur.value == "/" || p.cur.value == "%" || p.cur.value == "^") {
op := p.cur.value
p.advance()
right, err := p.parseUnary()
if err != nil {
return 0, err
}
switch op {
case "*":
left *= right
case "/":
if right == 0 {
return 0, fmt.Errorf("除数不能为零")
}
left /= right
case "%":
left = math.Mod(left, right)
case "^":
left = math.Pow(left, right)
}
}
return left, nil
}
// parseUnary handles unary plus/minus.
func (p *parser) parseUnary() (float64, error) {
if p.cur.kind == tokOp && p.cur.value == "-" {
p.advance()
val, err := p.parseUnary()
if err != nil {
return 0, err
}
return -val, nil
}
if p.cur.kind == tokOp && p.cur.value == "+" {
p.advance()
return p.parseUnary()
}
return p.parseAtom()
}
// parseAtom handles numbers, parenthesized expressions, and function calls.
func (p *parser) parseAtom() (float64, error) {
switch p.cur.kind {
case tokNumber:
val, err := strconv.ParseFloat(p.cur.value, 64)
if err != nil {
return 0, fmt.Errorf("无效数字: %s", p.cur.value)
}
p.advance()
return val, nil
case tokIdent:
name := strings.ToLower(p.cur.value)
p.advance()
// constants
switch name {
case "pi":
return math.Pi, nil
case "e":
return math.E, nil
}
// function call
if p.cur.kind != tokLParen {
return 0, fmt.Errorf("未知标识符: %s (如果是函数需要加括号)", name)
}
p.advance() // consume '('
arg, err := p.parseExpression()
if err != nil {
return 0, err
}
if p.cur.kind != tokRParen {
return 0, fmt.Errorf("函数 %s 缺少右括号", name)
}
p.advance() // consume ')'
return applyFunc(name, arg)
case tokLParen:
p.advance() // consume '('
val, err := p.parseExpression()
if err != nil {
return 0, err
}
if p.cur.kind != tokRParen {
return 0, fmt.Errorf("缺少右括号")
}
p.advance() // consume ')'
return val, nil
default:
return 0, fmt.Errorf("意外的 token: %v", p.cur.value)
}
}
// applyFunc applies a named mathematical function to an argument.
func applyFunc(name string, arg float64) (float64, error) {
switch name {
case "sqrt":
if arg < 0 {
return 0, fmt.Errorf("sqrt 参数不能为负数")
}
return math.Sqrt(arg), nil
case "sin":
return math.Sin(arg), nil
case "cos":
return math.Cos(arg), nil
case "tan":
return math.Tan(arg), nil
case "abs":
return math.Abs(arg), nil
case "floor":
return math.Floor(arg), nil
case "ceil":
return math.Ceil(arg), nil
case "round":
return math.Round(arg), nil
case "log":
if arg <= 0 {
return 0, fmt.Errorf("log 参数必须大于0")
}
return math.Log10(arg), nil
case "ln":
if arg <= 0 {
return 0, fmt.Errorf("ln 参数必须大于0")
}
return math.Log(arg), nil
case "pow":
return 0, fmt.Errorf("pow 需要两个参数,请使用 ^ 运算符代替")
default:
return 0, fmt.Errorf("未知函数: %s", name)
}
}
@@ -0,0 +1,209 @@
package tools
import (
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"fmt"
"hash"
"net/url"
)
// CryptoTool provides cryptographic and encoding utilities for the LLM.
// Supports hashing, base64, and URL encoding.
type CryptoTool struct{}
// NewCryptoTool creates a crypto/encoding tool.
func NewCryptoTool() *CryptoTool {
return &CryptoTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *CryptoTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "crypto",
Description: "加密哈希与编码工具。计算MD5/SHA哈希值,执行Base64编码/解码,URL编码/解码。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"hash", "base64_encode", "base64_decode", "url_encode", "url_decode"},
"description": "操作类型。hash: 计算哈希值;base64_encode: Base64编码;base64_decode: Base64解码;url_encode: URL编码;url_decode: URL解码",
},
"input": map[string]interface{}{
"type": "string",
"description": "输入数据,需要处理的字符串",
},
"algorithm": map[string]interface{}{
"type": "string",
"enum": []string{"md5", "sha1", "sha256", "sha512"},
"description": "哈希算法(用于 hash 操作),默认 sha256",
},
},
"required": []string{"action", "input"},
},
}
}
// Execute performs crypto/encoding operations.
func (t *CryptoTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "crypto",
Success: false,
Error: "缺少 action 参数",
}, nil
}
input, ok := arguments["input"].(string)
if !ok {
return &ToolResult{
ToolName: "crypto",
Success: false,
Error: "缺少 input 参数",
}, nil
}
switch action {
case "hash":
return t.handleHash(arguments)
case "base64_encode":
return t.handleBase64Encode(input)
case "base64_decode":
return t.handleBase64Decode(input)
case "url_encode":
return t.handleURLEncode(input)
case "url_decode":
return t.handleURLDecode(input)
default:
return &ToolResult{
ToolName: "crypto",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: hash, base64_encode, base64_decode, url_encode, url_decode", action),
}, nil
}
}
// handleHash computes a hash of the input using the specified algorithm.
func (t *CryptoTool) handleHash(arguments map[string]interface{}) (*ToolResult, error) {
input, _ := arguments["input"].(string)
algorithm, _ := arguments["algorithm"].(string)
if algorithm == "" {
algorithm = "sha256"
}
var h hash.Hash
switch algorithm {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return &ToolResult{
ToolName: "crypto",
Success: false,
Error: fmt.Sprintf("不支持的哈希算法: %s,支持: md5, sha1, sha256, sha512", algorithm),
}, nil
}
h.Write([]byte(input))
hashBytes := h.Sum(nil)
hashHex := fmt.Sprintf("%x", hashBytes)
return &ToolResult{
ToolName: "crypto",
Success: true,
Data: fmt.Sprintf("哈希算法: %s\n输入长度: %d 字节\n哈希值 (hex): %s\n哈希长度: %d 位",
algorithm, len(input), hashHex, len(hashBytes)*8),
}, nil
}
// handleBase64Encode encodes input to Base64.
func (t *CryptoTool) handleBase64Encode(input string) (*ToolResult, error) {
encoded := base64.StdEncoding.EncodeToString([]byte(input))
return &ToolResult{
ToolName: "crypto",
Success: true,
Data: fmt.Sprintf("Base64 编码结果:\n原始 (%d 字节): %s\n编码 (%d 字符): %s",
len(input), truncate(input, 100), len(encoded), encoded),
}, nil
}
// handleBase64Decode decodes a Base64 string.
func (t *CryptoTool) handleBase64Decode(input string) (*ToolResult, error) {
// Try standard encoding first, then URL-safe
decoded, err := base64.StdEncoding.DecodeString(input)
if err != nil {
decoded, err = base64.RawStdEncoding.DecodeString(input)
if err != nil {
decoded, err = base64.URLEncoding.DecodeString(input)
if err != nil {
decoded, err = base64.RawURLEncoding.DecodeString(input)
if err != nil {
return &ToolResult{
ToolName: "crypto",
Success: false,
Error: fmt.Sprintf("Base64 解码失败: 输入不是有效的 Base64 字符串"),
}, nil
}
}
}
}
return &ToolResult{
ToolName: "crypto",
Success: true,
Data: fmt.Sprintf("Base64 解码结果:\n原始 (%d 字符): %s\n解码 (%d 字节): %s",
len(input), truncate(input, 100), len(decoded), truncate(string(decoded), 200)),
}, nil
}
// handleURLEncode URL-encodes the input string.
func (t *CryptoTool) handleURLEncode(input string) (*ToolResult, error) {
encoded := url.QueryEscape(input)
return &ToolResult{
ToolName: "crypto",
Success: true,
Data: fmt.Sprintf("URL 编码结果:\n原始 (%d 字节): %s\n编码 (%d 字节): %s",
len(input), truncate(input, 100), len(encoded), encoded),
}, nil
}
// handleURLDecode URL-decodes the input string.
func (t *CryptoTool) handleURLDecode(input string) (*ToolResult, error) {
decoded, err := url.QueryUnescape(input)
if err != nil {
return &ToolResult{
ToolName: "crypto",
Success: false,
Error: fmt.Sprintf("URL 解码失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "crypto",
Success: true,
Data: fmt.Sprintf("URL 解码结果:\n原始 (%d 字节): %s\n解码 (%d 字节): %s",
len(input), truncate(input, 100), len(decoded), truncate(decoded, 200)),
}, nil
}
// truncate truncates a string to maxLen characters, adding "..." if truncated.
func truncate(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen]) + "..."
}
@@ -0,0 +1,426 @@
package tools
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"unicode"
)
// DateTimeTool provides date/time operations for the LLM.
// Supports current time, formatting, date arithmetic, and timezone listing.
type DateTimeTool struct{}
// NewDateTimeTool creates a date/time tool.
func NewDateTimeTool() *DateTimeTool {
return &DateTimeTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *DateTimeTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "datetime",
Description: "日期时间工具。获取当前时间、格式化日期、日期加减、计算日期差、查看可用时区。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"now", "format", "add", "diff", "timezone_list"},
"description": "操作类型。now: 获取当前时间;format: 格式化日期;add: 日期加减;diff: 计算两个日期的差值;timezone_list: 列出常用时区",
},
"format": map[string]interface{}{
"type": "string",
"description": "日期格式串(Go风格)。默认 \"2006-01-02 15:04:05\"。常用: \"2006-01-02\"(仅日期)、\"15:04:05\"(仅时间)",
},
"timezone": map[string]interface{}{
"type": "string",
"description": "时区标识,如 \"Asia/Shanghai\"、\"America/New_York\"、\"UTC\"。默认使用服务器本地时区",
},
"date": map[string]interface{}{
"type": "string",
"description": "基准日期,格式为 \"2006-01-02 15:04:05\" 或 \"2006-01-02\"",
},
"duration": map[string]interface{}{
"type": "string",
"description": "时长字符串,如 \"24h\"、\"7d\"、\"30m\"、\"1h30m\"。支持单位: s(秒), m(分钟), h(小时), d(天), w(周), M(月), y(年)",
},
"date2": map[string]interface{}{
"type": "string",
"description": "第二个日期(用于 diff 操作),格式同 date",
},
},
"required": []string{"action"},
},
}
}
// Execute performs date/time operations.
func (t *DateTimeTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: "缺少 action 参数",
}, nil
}
switch action {
case "now":
return t.handleNow(arguments)
case "format":
return t.handleFormat(arguments)
case "add":
return t.handleAdd(arguments)
case "diff":
return t.handleDiff(arguments)
case "timezone_list":
return t.handleTimezoneList()
default:
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: now, format, add, diff, timezone_list", action),
}, nil
}
}
// handleNow returns the current date/time in the specified timezone.
func (t *DateTimeTool) handleNow(arguments map[string]interface{}) (*ToolResult, error) {
tz, err := t.getTimezone(arguments)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: err.Error(),
}, nil
}
format := t.getFormat(arguments)
now := time.Now().In(tz)
return &ToolResult{
ToolName: "datetime",
Success: true,
Data: fmt.Sprintf("当前时间: %s\n时区: %s\nUnix时间戳: %d",
now.Format(format), tz.String(), now.Unix()),
}, nil
}
// handleFormat formats a given date string.
func (t *DateTimeTool) handleFormat(arguments map[string]interface{}) (*ToolResult, error) {
dateStr, _ := arguments["date"].(string)
if dateStr == "" {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: "format 操作需要 date 参数",
}, nil
}
parsed, err := t.parseDate(dateStr)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: fmt.Sprintf("日期解析失败: %v", err),
}, nil
}
tz, err := t.getTimezone(arguments)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: err.Error(),
}, nil
}
format := t.getFormat(arguments)
formatted := parsed.In(tz).Format(format)
return &ToolResult{
ToolName: "datetime",
Success: true,
Data: fmt.Sprintf("原始: %s\n格式化: %s\n时区: %s", dateStr, formatted, tz.String()),
}, nil
}
// handleAdd adds/subtracts a duration from a date.
func (t *DateTimeTool) handleAdd(arguments map[string]interface{}) (*ToolResult, error) {
durationStr, _ := arguments["duration"].(string)
if durationStr == "" {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: "add 操作需要 duration 参数",
}, nil
}
dateStr, _ := arguments["date"].(string)
var base time.Time
if dateStr != "" {
var err error
base, err = t.parseDate(dateStr)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: fmt.Sprintf("日期解析失败: %v", err),
}, nil
}
} else {
tz, _ := t.getTimezone(arguments)
base = time.Now().In(tz)
}
dur, err := t.parseDuration(durationStr)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: fmt.Sprintf("时长解析失败: %v", err),
}, nil
}
tz, _ := t.getTimezone(arguments)
result := base.In(tz)
// Extract months and years from the duration string (not handled by time.Duration)
months := extractDurationUnit(durationStr, 'M')
years := extractDurationUnit(durationStr, 'y')
if months != 0 || years != 0 {
result = result.AddDate(years, months, 0)
}
// Add the standard duration part
if dur != 0 {
result = result.Add(dur)
}
format := t.getFormat(arguments)
return &ToolResult{
ToolName: "datetime",
Success: true,
Data: fmt.Sprintf("基准日期: %s\n操作: %s\n结果: %s",
base.In(tz).Format(format), durationStr, result.Format(format)),
}, nil
}
// handleDiff calculates the difference between two dates.
func (t *DateTimeTool) handleDiff(arguments map[string]interface{}) (*ToolResult, error) {
dateStr, _ := arguments["date"].(string)
date2Str, _ := arguments["date2"].(string)
if dateStr == "" || date2Str == "" {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: "diff 操作需要 date 和 date2 参数",
}, nil
}
d1, err := t.parseDate(dateStr)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: fmt.Sprintf("date 解析失败: %v", err),
}, nil
}
d2, err := t.parseDate(date2Str)
if err != nil {
return &ToolResult{
ToolName: "datetime",
Success: false,
Error: fmt.Sprintf("date2 解析失败: %v", err),
}, nil
}
diff := d2.Sub(d1)
absDiff := diff
if absDiff < 0 {
absDiff = -absDiff
}
days := int(absDiff.Hours() / 24)
hours := int(absDiff.Hours()) % 24
minutes := int(absDiff.Minutes()) % 60
seconds := int(absDiff.Seconds()) % 60
sign := ""
if diff < 0 {
sign = "-"
}
return &ToolResult{
ToolName: "datetime",
Success: true,
Data: fmt.Sprintf("日期1: %s\n日期2: %s\n差值: %s%d天 %d小时 %d分钟 %d秒 (总计 %s%.0f秒)",
dateStr, date2Str, sign, days, hours, minutes, seconds, sign, absDiff.Seconds()),
}, nil
}
// handleTimezoneList returns a list of common timezones.
func (t *DateTimeTool) handleTimezoneList() (*ToolResult, error) {
zones := []string{
"UTC",
"Asia/Shanghai (北京时间)",
"Asia/Tokyo (东京时间)",
"Asia/Seoul (首尔时间)",
"Asia/Singapore (新加坡时间)",
"Asia/Kolkata (印度时间)",
"Asia/Dubai (迪拜时间)",
"Europe/London (伦敦时间)",
"Europe/Paris (巴黎时间)",
"Europe/Berlin (柏林时间)",
"Europe/Moscow (莫斯科时间)",
"America/New_York (纽约时间)",
"America/Chicago (芝加哥时间)",
"America/Denver (丹佛时间)",
"America/Los_Angeles (洛杉矶时间)",
"America/Sao_Paulo (圣保罗时间)",
"Australia/Sydney (悉尼时间)",
"Pacific/Auckland (奥克兰时间)",
}
var result strings.Builder
result.WriteString("常用时区列表:\n\n")
for i, z := range zones {
result.WriteString(fmt.Sprintf(" %2d. %s\n", i+1, z))
}
return &ToolResult{
ToolName: "datetime",
Success: true,
Data: result.String(),
}, nil
}
// getTimezone extracts the timezone from arguments, defaulting to local.
func (t *DateTimeTool) getTimezone(arguments map[string]interface{}) (*time.Location, error) {
tzName, _ := arguments["timezone"].(string)
if tzName == "" {
return time.Local, nil
}
loc, err := time.LoadLocation(tzName)
if err != nil {
return nil, fmt.Errorf("无效时区: %s", tzName)
}
return loc, nil
}
// getFormat extracts the format string from arguments, defaulting to standard format.
func (t *DateTimeTool) getFormat(arguments map[string]interface{}) string {
format, _ := arguments["format"].(string)
if format == "" {
return "2006-01-02 15:04:05"
}
return format
}
// parseDate parses a date string with multiple format attempts.
func (t *DateTimeTool) parseDate(s string) (time.Time, error) {
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05",
"2006-01-02",
"2006/01/02 15:04:05",
"2006/01/02",
time.RFC3339,
time.RFC3339Nano,
}
for _, f := range formats {
if t, err := time.Parse(f, s); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("无法解析日期: %s", s)
}
// parseDuration parses a human-friendly duration string like "24h", "7d", "1h30m".
func (t *DateTimeTool) parseDuration(s string) (time.Duration, error) {
// First try standard Go duration parsing
if d, err := time.ParseDuration(s); err == nil {
return d, nil
}
// Custom parsing for days and weeks
var total time.Duration
remaining := s
for len(remaining) > 0 {
// find the number
numStart := 0
for numStart < len(remaining) && !unicode.IsDigit(rune(remaining[numStart])) && remaining[numStart] != '-' {
numStart++
}
if numStart >= len(remaining) {
break
}
numEnd := numStart
for numEnd < len(remaining) && (unicode.IsDigit(rune(remaining[numEnd])) || remaining[numEnd] == '.') {
numEnd++
}
val, err := strconv.ParseFloat(remaining[numStart:numEnd], 64)
if err != nil {
return 0, fmt.Errorf("无效时长数字: %s", remaining[numStart:numEnd])
}
unitEnd := numEnd
for unitEnd < len(remaining) && unicode.IsLetter(rune(remaining[unitEnd])) {
unitEnd++
}
unit := remaining[numEnd:unitEnd]
switch unit {
case "s":
total += time.Duration(val * float64(time.Second))
case "m":
total += time.Duration(val * float64(time.Minute))
case "h":
total += time.Duration(val * float64(time.Hour))
case "d":
total += time.Duration(val * 24 * float64(time.Hour))
case "w":
total += time.Duration(val * 7 * 24 * float64(time.Hour))
default:
// skip unknown units (M and y handled elsewhere)
}
remaining = remaining[unitEnd:]
}
return total, nil
}
// extractDurationUnit extracts numeric value for a given unit character from a duration string.
// e.g., extractDurationUnit("3M", 'M') returns 3, extractDurationUnit("1y2M", 'y') returns 1.
func extractDurationUnit(s string, unit byte) int {
for i := 0; i < len(s); i++ {
if s[i] == unit {
// Scan backwards to find the start of the number
j := i - 1
for j >= 0 && (unicode.IsDigit(rune(s[j])) || s[j] == '.') {
j--
}
numStr := s[j+1 : i]
val, err := strconv.Atoi(numStr)
if err != nil {
return 0
}
return val
}
}
return 0
}
+333
View File
@@ -0,0 +1,333 @@
package tools
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
)
// FileTool provides sandboxed file system operations for the LLM.
// All paths are restricted to a DATA_DIR to prevent directory traversal attacks.
type FileTool struct {
dataDir string
}
// NewFileTool creates a file operation tool with the given data directory.
func NewFileTool(dataDir string) *FileTool {
if dataDir == "" {
dataDir = "/tmp/cyrene_data"
}
return &FileTool{dataDir: dataDir}
}
// Definition returns the tool definition for LLM function calling.
func (t *FileTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "file_ops",
Description: "文件操作工具。在服务端安全沙盒内读写文件、列出目录、检查文件是否存在、删除文件。所有操作限制在数据目录内,无法访问系统文件。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"read", "write", "list", "exists", "delete"},
"description": "操作类型。read: 读取文件;write: 写入文件(覆盖或创建);list: 列出目录内容;exists: 检查路径是否存在;delete: 删除文件",
},
"path": map[string]interface{}{
"type": "string",
"description": "文件或目录路径(相对于数据目录),如 \"notes/todo.txt\"",
},
"content": map[string]interface{}{
"type": "string",
"description": "写入内容(write 操作时必需)",
},
},
"required": []string{"action", "path"},
},
}
}
// Execute performs file operations.
func (t *FileTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: "缺少 action 参数",
}, nil
}
relPath, ok := arguments["path"].(string)
if !ok || relPath == "" {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: "缺少 path 参数",
}, nil
}
safePath, err := t.resolveSafePath(relPath)
if err != nil {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: err.Error(),
}, nil
}
switch action {
case "read":
return t.handleRead(safePath, relPath)
case "write":
content, _ := arguments["content"].(string)
return t.handleWrite(safePath, relPath, content)
case "list":
return t.handleList(safePath, relPath)
case "exists":
return t.handleExists(safePath, relPath)
case "delete":
return t.handleDelete(safePath, relPath)
default:
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: read, write, list, exists, delete", action),
}, nil
}
}
// resolveSafePath resolves a relative path and ensures it stays within dataDir.
func (t *FileTool) resolveSafePath(relPath string) (string, error) {
// Clean the path first
clean := filepath.Clean(relPath)
// Ensure data directory exists
if err := os.MkdirAll(t.dataDir, 0755); err != nil {
return "", fmt.Errorf("创建数据目录失败: %v", err)
}
abs := filepath.Join(t.dataDir, clean)
// Prevent directory traversal
realPath, err := filepath.EvalSymlinks(abs)
if err != nil {
// If the path doesn't exist yet, we can still check the prefix
if os.IsNotExist(err) {
// Ensure the resolved path (without symlinks) is within dataDir
if !strings.HasPrefix(filepath.Clean(abs), filepath.Clean(t.dataDir)+string(filepath.Separator)) &&
filepath.Clean(abs) != filepath.Clean(t.dataDir) {
return "", fmt.Errorf("路径穿越检测: %s 不在允许的数据目录内", relPath)
}
return abs, nil
}
return "", fmt.Errorf("路径解析失败: %v", err)
}
if !strings.HasPrefix(realPath, filepath.Clean(t.dataDir)+string(filepath.Separator)) &&
realPath != filepath.Clean(t.dataDir) {
return "", fmt.Errorf("路径穿越检测: %s 不在允许的数据目录内", relPath)
}
return realPath, nil
}
// handleRead reads a file, limited to 100KB.
func (t *FileTool) handleRead(absPath, relPath string) (*ToolResult, error) {
const maxSize = 100 * 1024 // 100KB
info, err := os.Stat(absPath)
if err != nil {
if os.IsNotExist(err) {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("文件不存在: %s", relPath),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("读取文件失败: %v", err),
}, nil
}
if info.IsDir() {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("路径是目录,不能用 read 操作: %s", relPath),
}, nil
}
if info.Size() > maxSize {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("文件过大 (%d bytes),超过限制 (%d bytes)", info.Size(), maxSize),
}, nil
}
data, err := os.ReadFile(absPath)
if err != nil {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("读取文件失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: fmt.Sprintf("文件: %s\n大小: %d bytes\n---\n%s", relPath, len(data), string(data)),
}, nil
}
// handleWrite writes content to a file.
func (t *FileTool) handleWrite(absPath, relPath, content string) (*ToolResult, error) {
// Ensure parent directory exists
dir := filepath.Dir(absPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("创建目录失败: %v", err),
}, nil
}
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("写入文件失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: fmt.Sprintf("已写入文件: %s (%d bytes)", relPath, len(content)),
}, nil
}
// handleList lists directory contents.
func (t *FileTool) handleList(absPath, relPath string) (*ToolResult, error) {
entries, err := os.ReadDir(absPath)
if err != nil {
if os.IsNotExist(err) {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("目录不存在: %s", relPath),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("读取目录失败: %v", err),
}, nil
}
if len(entries) == 0 {
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: fmt.Sprintf("目录: %s\n(空目录)", relPath),
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("目录: %s\n共 %d 项:\n", relPath, len(entries)))
for _, entry := range entries {
icon := "📄"
if entry.IsDir() {
icon = "📁"
}
info, _ := entry.Info()
size := ""
if info != nil && !entry.IsDir() {
size = fmt.Sprintf(" (%d bytes)", info.Size())
}
result.WriteString(fmt.Sprintf(" %s %s%s\n", icon, entry.Name(), size))
}
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: result.String(),
}, nil
}
// handleExists checks whether a path exists.
func (t *FileTool) handleExists(absPath, relPath string) (*ToolResult, error) {
info, err := os.Stat(absPath)
if err != nil {
if os.IsNotExist(err) {
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: fmt.Sprintf("路径不存在: %s", relPath),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("检查路径失败: %v", err),
}, nil
}
kind := "文件"
if info.IsDir() {
kind = "目录"
}
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: fmt.Sprintf("路径存在: %s (%s, %d bytes)", relPath, kind, info.Size()),
}, nil
}
// handleDelete deletes a file.
func (t *FileTool) handleDelete(absPath, relPath string) (*ToolResult, error) {
info, err := os.Stat(absPath)
if err != nil {
if os.IsNotExist(err) {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("文件不存在: %s", relPath),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("删除文件失败: %v", err),
}, nil
}
if info.IsDir() {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("不能删除目录(安全限制): %s", relPath),
}, nil
}
if err := os.Remove(absPath); err != nil {
return &ToolResult{
ToolName: "file_ops",
Success: false,
Error: fmt.Sprintf("删除文件失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "file_ops",
Success: true,
Data: fmt.Sprintf("已删除文件: %s", relPath),
}, nil
}
+190
View File
@@ -0,0 +1,190 @@
package tools
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// HTTPTool sends arbitrary HTTP requests, more flexible than web_fetch.
// Supports custom methods, headers, and body.
type HTTPTool struct {
client *http.Client
}
// NewHTTPTool creates an HTTP request tool.
func NewHTTPTool() *HTTPTool {
return &HTTPTool{
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// Definition returns the tool definition for LLM function calling.
func (t *HTTPTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "http_request",
Description: "发送任意HTTP请求。比web_fetch更灵活,支持自定义请求方法、请求头和请求体。返回状态码、响应头和响应体。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"url": map[string]interface{}{
"type": "string",
"description": "请求URL,必须是完整的 http:// 或 https:// 链接",
},
"method": map[string]interface{}{
"type": "string",
"enum": []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"},
"description": "HTTP方法,默认GET",
},
"headers": map[string]interface{}{
"type": "object",
"description": "请求头,键值对格式,如 {\"Content-Type\": \"application/json\", \"Authorization\": \"Bearer token123\"}",
},
"body": map[string]interface{}{
"type": "string",
"description": "请求体内容",
},
"timeout": map[string]interface{}{
"type": "number",
"description": "超时秒数,默认10秒",
},
},
"required": []string{"url"},
},
}
}
// Execute sends an HTTP request.
func (t *HTTPTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
url, ok := arguments["url"].(string)
if !ok || url == "" {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: "缺少 url 参数",
}, nil
}
// Security: only allow HTTP/HTTPS
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: "仅支持 http:// 或 https:// 链接",
}, nil
}
method, _ := arguments["method"].(string)
if method == "" {
method = "GET"
}
method = strings.ToUpper(method)
// Validate method
validMethods := map[string]bool{
"GET": true, "POST": true, "PUT": true, "DELETE": true,
"PATCH": true, "HEAD": true, "OPTIONS": true,
}
if !validMethods[method] {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("不支持的HTTP方法: %s", method),
}, nil
}
// Handle timeout
timeoutSec := 10.0
if timeoutVal, ok := arguments["timeout"].(float64); ok && timeoutVal > 0 {
timeoutSec = timeoutVal
}
// Create a client with the specified timeout
client := &http.Client{
Timeout: time.Duration(timeoutSec * float64(time.Second)),
}
// Build body reader
var bodyReader io.Reader
bodyStr, _ := arguments["body"].(string)
if bodyStr != "" {
bodyReader = strings.NewReader(bodyStr)
}
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("创建请求失败: %v", err),
}, nil
}
// Set default User-Agent
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyreneBot/1.0)")
// Parse custom headers
if headersRaw, ok := arguments["headers"].(map[string]interface{}); ok {
for k, v := range headersRaw {
val, ok := v.(string)
if !ok {
val = fmt.Sprintf("%v", v)
}
req.Header.Set(k, val)
}
}
resp, err := client.Do(req)
if err != nil {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("请求失败: %v", err),
}, nil
}
defer resp.Body.Close()
// Read response body (limited to 50KB)
const maxBodySize = 50 * 1024
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxBodySize)))
if err != nil {
return &ToolResult{
ToolName: "http_request",
Success: false,
Error: fmt.Sprintf("读取响应失败: %v", err),
}, nil
}
// Build response headers string
var headerLines []string
for k, vals := range resp.Header {
for _, v := range vals {
headerLines = append(headerLines, fmt.Sprintf("%s: %s", k, v))
}
}
headersStr := strings.Join(headerLines, "\n")
bodyTruncated := ""
if len(bodyBytes) > maxBodySize {
bodyTruncated = fmt.Sprintf("\n... [响应体已截断,原大小约 %d bytes]", len(bodyBytes))
}
result := fmt.Sprintf(
"请求: %s %s\n状态: %d %s\n响应头:\n%s\n\n响应体 (%d bytes):\n%s%s",
method, url,
resp.StatusCode, resp.Status,
headersStr,
len(bodyBytes), string(bodyBytes), bodyTruncated,
)
return &ToolResult{
ToolName: "http_request",
Success: resp.StatusCode < 500,
Data: result,
}, nil
}
+228
View File
@@ -0,0 +1,228 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
)
// JSONTool provides JSON parsing, querying, and validation for the LLM.
type JSONTool struct{}
// NewJSONTool creates a JSON processing tool.
func NewJSONTool() *JSONTool {
return &JSONTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *JSONTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "json_ops",
Description: "JSON处理工具。解析JSON字符串并格式化输出、用简单路径查询JSON字段、验证JSON是否合法。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"parse", "query", "validate"},
"description": "操作类型。parse: 解析JSON并格式化输出;query: 用路径查询JSON中的值(如\"users.0.name\"表示取users数组第0个元素的name字段);validate: 验证JSON字符串是否合法",
},
"json_string": map[string]interface{}{
"type": "string",
"description": "JSON字符串",
},
"path": map[string]interface{}{
"type": "string",
"description": "查询路径(query操作时使用)。支持点分隔和数组索引,如 \"users.0.name\"、\"data.list.2.title\"",
},
},
"required": []string{"action", "json_string"},
},
}
}
// Execute performs JSON operations.
func (t *JSONTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: "缺少 action 参数",
}, nil
}
jsonStr, ok := arguments["json_string"].(string)
if !ok || jsonStr == "" {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: "缺少 json_string 参数",
}, nil
}
switch action {
case "parse":
return t.handleParse(jsonStr)
case "query":
path, _ := arguments["path"].(string)
return t.handleQuery(jsonStr, path)
case "validate":
return t.handleValidate(jsonStr)
default:
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: parse, query, validate", action),
}, nil
}
}
// handleParse parses a JSON string and returns a formatted version.
func (t *JSONTool) handleParse(jsonStr string) (*ToolResult, error) {
var data interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: fmt.Sprintf("JSON解析失败: %v", err),
}, nil
}
pretty, err := json.MarshalIndent(data, "", " ")
if err != nil {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: fmt.Sprintf("JSON格式化失败: %v", err),
}, nil
}
return &ToolResult{
ToolName: "json_ops",
Success: true,
Data: fmt.Sprintf("解析成功\n格式化输出:\n%s", string(pretty)),
}, nil
}
// handleQuery queries a JSON value by dot-notation path.
func (t *JSONTool) handleQuery(jsonStr, path string) (*ToolResult, error) {
if path == "" {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: "query 操作需要 path 参数",
}, nil
}
var data interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: fmt.Sprintf("JSON解析失败: %v", err),
}, nil
}
value, err := queryPath(data, path)
if err != nil {
return &ToolResult{
ToolName: "json_ops",
Success: false,
Error: err.Error(),
}, nil
}
pretty, err := json.MarshalIndent(value, "", " ")
if err != nil {
return &ToolResult{
ToolName: "json_ops",
Success: true,
Data: fmt.Sprintf("路径: %s\n值: %v", path, value),
}, nil
}
return &ToolResult{
ToolName: "json_ops",
Success: true,
Data: fmt.Sprintf("路径: %s\n值:\n%s", path, string(pretty)),
}, nil
}
// handleValidate validates whether a string is valid JSON.
func (t *JSONTool) handleValidate(jsonStr string) (*ToolResult, error) {
var data interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
// Try to give a helpful error message
errStr := err.Error()
// Extract line/position info if available
return &ToolResult{
ToolName: "json_ops",
Success: true,
Data: fmt.Sprintf("❌ JSON不合法\n错误: %s", errStr),
}, nil
}
// Determine JSON type
typeName := "object"
switch data.(type) {
case []interface{}:
typeName = "array"
case string:
typeName = "string"
case float64:
typeName = "number"
case bool:
typeName = "boolean"
case nil:
typeName = "null"
}
size := len(jsonStr)
return &ToolResult{
ToolName: "json_ops",
Success: true,
Data: fmt.Sprintf("✅ JSON合法\n类型: %s\n大小: %d bytes", typeName, size),
}, nil
}
// queryPath traverses a JSON value using dot-notation and array index syntax.
// Examples: "users.0.name", "data.list", "items.2"
func queryPath(data interface{}, path string) (interface{}, error) {
// Remove leading "$." if present (JSONPath style)
path = strings.TrimPrefix(path, "$.")
if path == "" || path == "$" {
return data, nil
}
parts := strings.Split(path, ".")
current := data
for _, part := range parts {
switch v := current.(type) {
case map[string]interface{}:
var ok bool
current, ok = v[part]
if !ok {
return nil, fmt.Errorf("路径 '%s' 中字段 '%s' 不存在", path, part)
}
case []interface{}:
idx, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("路径 '%s' 中 '%s' 不是有效的数组索引", path, part)
}
if idx < 0 || idx >= len(v) {
return nil, fmt.Errorf("路径 '%s' 中索引 %d 越界(数组长度 %d)", path, idx, len(v))
}
current = v[idx]
default:
return nil, fmt.Errorf("路径 '%s' 中无法继续导航:'%s' 不是对象或数组", path, part)
}
}
return current, nil
}
@@ -0,0 +1,427 @@
package tools
import (
"context"
"fmt"
"regexp"
"strings"
)
// MarkdownTool provides Markdown processing utilities for the LLM.
// Supports HTML conversion, plain text extraction, link/code extraction, and TOC generation.
type MarkdownTool struct{}
// NewMarkdownTool creates a Markdown processing tool.
func NewMarkdownTool() *MarkdownTool {
return &MarkdownTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *MarkdownTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "markdown",
Description: "Markdown处理工具。将Markdown转为HTML、提取纯文本、提取链接/代码块、生成目录。用于处理Markdown格式的文档内容。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"to_html", "to_text", "extract_links", "extract_code", "table_of_contents"},
"description": "操作类型。to_html: 转换为HTMLto_text: 提取纯文本;extract_links: 提取所有链接;extract_code: 提取所有代码块;table_of_contents: 生成目录",
},
"markdown": map[string]interface{}{
"type": "string",
"description": "Markdown格式文本,需要处理的Markdown内容",
},
},
"required": []string{"action", "markdown"},
},
}
}
// Execute performs Markdown processing operations.
func (t *MarkdownTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "markdown",
Success: false,
Error: "缺少 action 参数",
}, nil
}
md, ok := arguments["markdown"].(string)
if !ok || strings.TrimSpace(md) == "" {
return &ToolResult{
ToolName: "markdown",
Success: false,
Error: "缺少 markdown 参数或内容为空",
}, nil
}
switch action {
case "to_html":
return t.handleToHTML(md)
case "to_text":
return t.handleToText(md)
case "extract_links":
return t.handleExtractLinks(md)
case "extract_code":
return t.handleExtractCode(md)
case "table_of_contents":
return t.handleTableOfContents(md)
default:
return &ToolResult{
ToolName: "markdown",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: to_html, to_text, extract_links, extract_code, table_of_contents", action),
}, nil
}
}
// handleToHTML converts Markdown to HTML using simple regex-based approach.
func (t *MarkdownTool) handleToHTML(md string) (*ToolResult, error) {
html := md
// Process in order: code blocks first (to avoid interference), then inline elements, then blocks
// 1. Code blocks (```...```) - preserve with placeholder
codeBlocks := make([]string, 0)
reFence := regexp.MustCompile("(?s)```[^`]*```")
html = reFence.ReplaceAllStringFunc(html, func(match string) string {
codeBlocks = append(codeBlocks, match)
return fmt.Sprintf("\x00CODEBLOCK%d\x00", len(codeBlocks)-1)
})
// 2. Inline code (`...`)
inlineCodes := make([]string, 0)
reInlineCode := regexp.MustCompile("`[^`]+`")
html = reInlineCode.ReplaceAllStringFunc(html, func(match string) string {
inlineCodes = append(inlineCodes, match)
return fmt.Sprintf("\x00INLINECODE%d\x00", len(inlineCodes)-1)
})
// 3. Images ![alt](url)
reImage := regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
html = reImage.ReplaceAllString(html, `<img src="$2" alt="$1">`)
// 4. Links [text](url)
reLink := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
html = reLink.ReplaceAllString(html, `<a href="$2">$1</a>`)
// 5. Bold **text** or __text__
reBold := regexp.MustCompile(`\*\*([^*]+)\*\*`)
html = reBold.ReplaceAllString(html, `<strong>$1</strong>`)
reBold2 := regexp.MustCompile(`__([^_]+)__`)
html = reBold2.ReplaceAllString(html, `<strong>$1</strong>`)
// 6. Italic *text* or _text_
reItalic := regexp.MustCompile(`\*([^*]+)\*`)
html = reItalic.ReplaceAllString(html, `<em>$1</em>`)
reItalic2 := regexp.MustCompile(`_([^_]+)_`)
html = reItalic2.ReplaceAllString(html, `<em>$1</em>`)
// 7. Strikethrough ~~text~~
reStrike := regexp.MustCompile(`~~([^~]+)~~`)
html = reStrike.ReplaceAllString(html, `<del>$1</del>`)
// 8. Headings (# to ######)
reH6 := regexp.MustCompile(`(?m)^######\s+(.+)$`)
html = reH6.ReplaceAllString(html, `<h6>$1</h6>`)
reH5 := regexp.MustCompile(`(?m)^#####\s+(.+)$`)
html = reH5.ReplaceAllString(html, `<h5>$1</h5>`)
reH4 := regexp.MustCompile(`(?m)^####\s+(.+)$`)
html = reH4.ReplaceAllString(html, `<h4>$1</h4>`)
reH3 := regexp.MustCompile(`(?m)^###\s+(.+)$`)
html = reH3.ReplaceAllString(html, `<h3>$1</h3>`)
reH2 := regexp.MustCompile(`(?m)^##\s+(.+)$`)
html = reH2.ReplaceAllString(html, `<h2>$1</h2>`)
reH1 := regexp.MustCompile(`(?m)^#\s+(.+)$`)
html = reH1.ReplaceAllString(html, `<h1>$1</h1>`)
// 9. Horizontal rules
reHR := regexp.MustCompile(`(?m)^(---|\*\*\*|___)\s*$`)
html = reHR.ReplaceAllString(html, `<hr>`)
// 10. Unordered lists (- item)
html = t.processLists(html, `(?m)^[\-*]\s+`, "ul")
// 11. Ordered lists (1. item)
html = t.processLists(html, `(?m)^\d+\.\s+`, "ol")
// 12. Blockquotes
reBlockquote := regexp.MustCompile(`(?m)^>\s?(.+)$`)
html = reBlockquote.ReplaceAllString(html, `<blockquote>$1</blockquote>`)
// 13. Paragraphs: wrap remaining text lines
html = t.wrapParagraphs(html)
// 14. Restore code blocks
for i, cb := range codeBlocks {
// Strip the opening/closing ```
content := strings.TrimPrefix(cb, "```")
content = strings.TrimSuffix(content, "```")
// Extract language if present on first line
lang := ""
content = strings.TrimSpace(content)
if idx := strings.Index(content, "\n"); idx > 0 {
lang = strings.TrimSpace(content[:idx])
content = strings.TrimSpace(content[idx+1:])
}
if lang != "" {
html = strings.ReplaceAll(html, fmt.Sprintf("\x00CODEBLOCK%d\x00", i),
fmt.Sprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, escapeHTML(content)))
} else {
html = strings.ReplaceAll(html, fmt.Sprintf("\x00CODEBLOCK%d\x00", i),
fmt.Sprintf("<pre><code>%s</code></pre>", escapeHTML(content)))
}
}
// 15. Restore inline code
for i, ic := range inlineCodes {
content := strings.Trim(ic, "`")
html = strings.ReplaceAll(html, fmt.Sprintf("\x00INLINECODE%d\x00", i),
fmt.Sprintf("<code>%s</code>", escapeHTML(content)))
}
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: html,
}, nil
}
// handleToText strips Markdown formatting and extracts plain text.
func (t *MarkdownTool) handleToText(md string) (*ToolResult, error) {
text := md
// Remove code blocks
reFence := regexp.MustCompile("(?s)```[^`]*```")
text = reFence.ReplaceAllString(text, "[代码块]")
// Remove inline code
reInlineCode := regexp.MustCompile("`[^`]+`")
text = reInlineCode.ReplaceAllString(text, "[代码]")
// Remove images ![alt](url) - keep alt text
reImage := regexp.MustCompile(`!\[([^\]]*)\]\([^)]+\)`)
text = reImage.ReplaceAllString(text, "$1")
// Remove links [text](url) - keep text
reLink := regexp.MustCompile(`\[([^\]]+)\]\([^)]+\)`)
text = reLink.ReplaceAllString(text, "$1")
// Remove bold/italic markers
text = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(text, "$1")
text = regexp.MustCompile(`__([^_]+)__`).ReplaceAllString(text, "$1")
text = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(text, "$1")
text = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(text, "$1")
// Remove strikethrough
text = regexp.MustCompile(`~~([^~]+)~~`).ReplaceAllString(text, "$1")
// Remove heading markers but keep the text
text = regexp.MustCompile(`(?m)^#{1,6}\s+`).ReplaceAllString(text, "")
// Remove horizontal rules
text = regexp.MustCompile(`(?m)^(---|\*\*\*|___)\s*$`).ReplaceAllString(text, "")
// Remove list markers
text = regexp.MustCompile(`(?m)^[\-*]\s+`).ReplaceAllString(text, "")
text = regexp.MustCompile(`(?m)^\d+\.\s+`).ReplaceAllString(text, "")
// Remove blockquote markers
text = regexp.MustCompile(`(?m)^>\s?`).ReplaceAllString(text, "")
// Collapse multiple blank lines
text = regexp.MustCompile(`\n{3,}`).ReplaceAllString(text, "\n\n")
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: fmt.Sprintf("纯文本提取结果 (%d 字符):\n\n%s",
len([]rune(text)), strings.TrimSpace(text)),
}, nil
}
// handleExtractLinks extracts all [text](url) links from Markdown.
func (t *MarkdownTool) handleExtractLinks(md string) (*ToolResult, error) {
reLink := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
matches := reLink.FindAllStringSubmatch(md, -1)
if len(matches) == 0 {
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: "未找到任何链接",
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("提取链接 (共 %d 个):\n\n", len(matches)))
for i, m := range matches {
result.WriteString(fmt.Sprintf("%d. [%s](%s)\n - 文本: %s\n - URL: %s\n\n",
i+1, m[1], m[2], m[1], m[2]))
}
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: strings.TrimSpace(result.String()),
}, nil
}
// handleExtractCode extracts all code blocks from Markdown.
func (t *MarkdownTool) handleExtractCode(md string) (*ToolResult, error) {
reFence := regexp.MustCompile("(?s)```([^`]*)```")
matches := reFence.FindAllStringSubmatch(md, -1)
if len(matches) == 0 {
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: "未找到任何代码块",
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("提取代码块 (共 %d 个):\n\n", len(matches)))
for i, m := range matches {
content := strings.TrimSpace(m[1])
lang := ""
if idx := strings.Index(content, "\n"); idx > 0 {
lang = strings.TrimSpace(content[:idx])
content = strings.TrimSpace(content[idx+1:])
}
result.WriteString(fmt.Sprintf("--- 代码块 %d", i+1))
if lang != "" {
result.WriteString(fmt.Sprintf(" (语言: %s)", lang))
}
result.WriteString(fmt.Sprintf(" ---\n%s\n\n", truncateText(content, 500)))
}
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: strings.TrimSpace(result.String()),
}, nil
}
// handleTableOfContents generates a table of contents from headings.
func (t *MarkdownTool) handleTableOfContents(md string) (*ToolResult, error) {
reHeading := regexp.MustCompile(`(?m)^(#{1,6})\s+(.+)$`)
matches := reHeading.FindAllStringSubmatch(md, -1)
if len(matches) == 0 {
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: "未找到任何标题,无法生成目录",
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("文档目录 (共 %d 个标题):\n\n", len(matches)))
for _, m := range matches {
level := len(m[1])
title := strings.TrimSpace(m[2])
indent := strings.Repeat(" ", level-1)
result.WriteString(fmt.Sprintf("%s%s %s\n", indent, strings.Repeat("#", level), title))
}
return &ToolResult{
ToolName: "markdown",
Success: true,
Data: result.String(),
}, nil
}
// --- Markdown helper functions below ---
// processLists wraps consecutive list items in <ul> or <ol> tags.
func (t *MarkdownTool) processLists(html, itemPattern, listTag string) string {
reItem := regexp.MustCompile(itemPattern + `(.+)$`)
lines := strings.Split(html, "\n")
result := make([]string, 0, len(lines))
inList := false
for _, line := range lines {
if reItem.MatchString(line) {
content := reItem.ReplaceAllString(line, "$1")
if !inList {
result = append(result, fmt.Sprintf("<%s>", listTag))
inList = true
}
result = append(result, fmt.Sprintf("<li>%s</li>", content))
} else {
if inList {
result = append(result, fmt.Sprintf("</%s>", listTag))
inList = false
}
result = append(result, line)
}
}
if inList {
result = append(result, fmt.Sprintf("</%s>", listTag))
}
return strings.Join(result, "\n")
}
// wrapParagraphs wraps non-tag lines in <p> tags.
func (t *MarkdownTool) wrapParagraphs(html string) string {
lines := strings.Split(html, "\n")
result := make([]string, 0, len(lines))
skipTags := map[string]bool{
"<h1>": true, "<h2>": true, "<h3>": true, "<h4>": true, "<h5>": true, "<h6>": true,
"<hr>": true, "<ul>": true, "</ul>": true, "<ol>": true, "</ol>": true,
"<li>": true, "</li>": true, "<blockquote>": true, "</blockquote>": true,
"<pre>": true, "</pre>": true, "<img": true,
}
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
result = append(result, line)
continue
}
// Check if line starts with an HTML tag
isTag := false
for tag := range skipTags {
if strings.HasPrefix(trimmed, tag) {
isTag = true
break
}
}
if !isTag {
result = append(result, fmt.Sprintf("<p>%s</p>", trimmed))
} else {
result = append(result, line)
}
}
return strings.Join(result, "\n")
}
// escapeHTML escapes special HTML characters.
func escapeHTML(s string) string {
replacer := strings.NewReplacer(
"&", "&"+"amp;",
"<", "&"+"lt;",
">", "&"+"gt;",
"\"", "&"+"quot;",
)
return replacer.Replace(s)
}
// truncateText truncates text to maxLen runes, adding "..." if truncated.
func truncateText(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen]) + "..."
}
@@ -0,0 +1,370 @@
package tools
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
mathrand "math/rand"
"strings"
)
// RandomTool provides random generation utilities for the LLM.
// Supports random numbers, UUIDs, passwords, and list operations.
type RandomTool struct{}
// NewRandomTool creates a random generation tool.
func NewRandomTool() *RandomTool {
return &RandomTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *RandomTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "random",
Description: "随机生成工具。生成随机数、UUID、安全密码,或从列表中随机选取/打乱元素。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"number", "uuid", "password", "pick", "shuffle"},
"description": "操作类型。number: 生成随机整数;uuid: 生成UUID v4password: 生成安全密码;pick: 从列表随机选取;shuffle: 随机打乱列表",
},
"min": map[string]interface{}{
"type": "number",
"description": "随机数最小值(用于 number 操作),默认 0",
},
"max": map[string]interface{}{
"type": "number",
"description": "随机数最大值(用于 number 操作),默认 100",
},
"length": map[string]interface{}{
"type": "integer",
"description": "密码长度(用于 password 操作),默认 16",
},
"items": map[string]interface{}{
"type": "array",
"description": "列表项(用于 pick/shuffle 操作),字符串数组",
"items": map[string]interface{}{
"type": "string",
},
},
"count": map[string]interface{}{
"type": "integer",
"description": "选取数量(用于 pick 操作),默认 1",
},
},
"required": []string{"action"},
},
}
}
// Execute performs random generation operations.
func (t *RandomTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "random",
Success: false,
Error: "缺少 action 参数",
}, nil
}
switch action {
case "number":
return t.handleNumber(arguments)
case "uuid":
return t.handleUUID()
case "password":
return t.handlePassword(arguments)
case "pick":
return t.handlePick(arguments)
case "shuffle":
return t.handleShuffle(arguments)
default:
return &ToolResult{
ToolName: "random",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: number, uuid, password, pick, shuffle", action),
}, nil
}
}
// handleNumber generates a random integer in [min, max].
func (t *RandomTool) handleNumber(arguments map[string]interface{}) (*ToolResult, error) {
minVal := getFloatArg(arguments, "min", 0)
maxVal := getFloatArg(arguments, "max", 100)
if minVal > maxVal {
minVal, maxVal = maxVal, minVal
}
minI := int64(minVal)
maxI := int64(maxVal)
// Use crypto/rand for secure random
rangeVal := maxI - minI + 1
if rangeVal <= 0 {
return &ToolResult{
ToolName: "random",
Success: false,
Error: "无效的数值范围",
}, nil
}
n, err := rand.Int(rand.Reader, big.NewInt(rangeVal))
if err != nil {
// Fallback to math/rand
result := minI + mathrand.Int63n(rangeVal)
return &ToolResult{
ToolName: "random",
Success: true,
Data: fmt.Sprintf("随机整数 [%d, %d]: %d", minI, maxI, result),
}, nil
}
result := minI + n.Int64()
return &ToolResult{
ToolName: "random",
Success: true,
Data: fmt.Sprintf("随机整数 [%d, %d]: %d", minI, maxI, result),
}, nil
}
// handleUUID generates a UUID v4 string.
func (t *RandomTool) handleUUID() (*ToolResult, error) {
uuid := make([]byte, 16)
_, err := rand.Read(uuid)
if err != nil {
return &ToolResult{
ToolName: "random",
Success: false,
Error: fmt.Sprintf("生成UUID失败: %v", err),
}, nil
}
// Set version 4 and variant bits
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant 10
uuidStr := fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16])
return &ToolResult{
ToolName: "random",
Success: true,
Data: fmt.Sprintf("UUID v4: %s", uuidStr),
}, nil
}
// handlePassword generates a secure random password.
func (t *RandomTool) handlePassword(arguments map[string]interface{}) (*ToolResult, error) {
length := getIntArg(arguments, "length", 16)
if length < 4 {
length = 16
}
if length > 128 {
length = 128
}
uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercase := "abcdefghijklmnopqrstuvwxyz"
digits := "0123456789"
symbols := "!@#$%^&*()_+-=[]{}|;:,.<>?"
allChars := uppercase + lowercase + digits + symbols
password := make([]byte, length)
// Ensure at least one of each character type
password[0] = uppercase[secureIndex(len(uppercase))]
password[1] = lowercase[secureIndex(len(lowercase))]
password[2] = digits[secureIndex(len(digits))]
password[3] = symbols[secureIndex(len(symbols))]
// Fill remaining with random characters from all sets
for i := 4; i < length; i++ {
password[i] = allChars[secureIndex(len(allChars))]
}
// Shuffle the password
shuffleBytes(password)
passwordStr := string(password)
return &ToolResult{
ToolName: "random",
Success: true,
Data: fmt.Sprintf("安全密码 (长度: %d):\n%s\n\n字符集: 大写字母 + 小写字母 + 数字 + 特殊符号",
length, passwordStr),
}, nil
}
// handlePick randomly picks items from a list.
func (t *RandomTool) handlePick(arguments map[string]interface{}) (*ToolResult, error) {
items := getStringSliceArg(arguments, "items")
if len(items) == 0 {
return &ToolResult{
ToolName: "random",
Success: false,
Error: "缺少 items 参数或列表为空",
}, nil
}
count := getIntArg(arguments, "count", 1)
if count < 1 {
count = 1
}
if count > len(items) {
count = len(items)
}
// Shuffle indices and pick first 'count'
indices := make([]int, len(items))
for i := range indices {
indices[i] = i
}
shuffleInts(indices)
picked := make([]string, 0, count)
for i := 0; i < count; i++ {
picked = append(picked, items[indices[i]])
}
var result strings.Builder
result.WriteString(fmt.Sprintf("从 %d 个选项中随机选取 %d 个:\n", len(items), count))
for i, p := range picked {
result.WriteString(fmt.Sprintf(" %d. %s\n", i+1, p))
}
return &ToolResult{
ToolName: "random",
Success: true,
Data: result.String(),
}, nil
}
// handleShuffle randomly shuffles a list.
func (t *RandomTool) handleShuffle(arguments map[string]interface{}) (*ToolResult, error) {
items := getStringSliceArg(arguments, "items")
if len(items) == 0 {
return &ToolResult{
ToolName: "random",
Success: false,
Error: "缺少 items 参数或列表为空",
}, nil
}
// Make a copy and shuffle
shuffled := make([]string, len(items))
copy(shuffled, items)
shuffleStrings(shuffled)
var result strings.Builder
result.WriteString(fmt.Sprintf("随机打乱结果 (共 %d 项):\n", len(shuffled)))
for i, s := range shuffled {
result.WriteString(fmt.Sprintf(" %d. %s\n", i+1, s))
}
return &ToolResult{
ToolName: "random",
Success: true,
Data: result.String(),
}, nil
}
// --- Helper functions ---
// getFloatArg extracts a float64 argument with fallback.
func getFloatArg(arguments map[string]interface{}, key string, fallback float64) float64 {
if v, ok := arguments[key]; ok {
switch val := v.(type) {
case float64:
return val
case int:
return float64(val)
case int64:
return float64(val)
case json.Number:
f, err := val.Float64()
if err == nil {
return f
}
}
}
return fallback
}
// getIntArg extracts an int argument with fallback.
func getIntArg(arguments map[string]interface{}, key string, fallback int) int {
if v, ok := arguments[key]; ok {
switch val := v.(type) {
case float64:
return int(val)
case int:
return val
case int64:
return int(val)
}
}
return fallback
}
// getStringSliceArg extracts a string slice argument.
func getStringSliceArg(arguments map[string]interface{}, key string) []string {
if v, ok := arguments[key]; ok {
switch val := v.(type) {
case []interface{}:
result := make([]string, 0, len(val))
for _, item := range val {
if s, ok := item.(string); ok {
result = append(result, s)
} else {
result = append(result, fmt.Sprintf("%v", item))
}
}
return result
case []string:
return val
}
}
return nil
}
// secureIndex returns a cryptographically secure random index in [0, max).
func secureIndex(max int) int {
if max <= 1 {
return 0
}
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return mathrand.Intn(max)
}
return int(n.Int64())
}
// shuffleBytes shuffles a byte slice using Fisher-Yates with crypto/rand.
func shuffleBytes(data []byte) {
for i := len(data) - 1; i > 0; i-- {
j := secureIndex(i + 1)
data[i], data[j] = data[j], data[i]
}
}
// shuffleInts shuffles an int slice using Fisher-Yates with crypto/rand.
func shuffleInts(data []int) {
for i := len(data) - 1; i > 0; i-- {
j := secureIndex(i + 1)
data[i], data[j] = data[j], data[i]
}
}
// shuffleStrings shuffles a string slice using Fisher-Yates with crypto/rand.
func shuffleStrings(data []string) {
for i := len(data) - 1; i > 0; i-- {
j := secureIndex(i + 1)
data[i], data[j] = data[j], data[i]
}
}
+345
View File
@@ -0,0 +1,345 @@
package tools
import (
"context"
"fmt"
"regexp"
"strings"
"unicode"
)
// TextTool provides text processing operations for the LLM.
// Supports counting, summarizing, translation, and pattern extraction.
type TextTool struct{}
// NewTextTool creates a text processing tool.
func NewTextTool() *TextTool {
return &TextTool{}
}
// Definition returns the tool definition for LLM function calling.
func (t *TextTool) Definition() ToolDefinition {
return ToolDefinition{
Name: "text",
Description: "文本处理工具。统计文本、生成摘要、翻译文本、正则提取信息。用于处理用户提供的文本内容。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"count", "summarize", "translate", "extract"},
"description": "操作类型。count: 统计字符/单词/行/段落数;summarize: 提取首段+关键句生成简单摘要;translate: 翻译文本(需指定target_lang);extract: 正则提取邮箱/电话/URL等",
},
"text": map[string]interface{}{
"type": "string",
"description": "输入文本,需要处理的文本内容",
},
"target_lang": map[string]interface{}{
"type": "string",
"enum": []string{"en", "zh", "ja", "ko", "fr", "de"},
"description": "翻译目标语言代码。en: 英语, zh: 中文, ja: 日语, ko: 韩语, fr: 法语, de: 德语",
},
"pattern": map[string]interface{}{
"type": "string",
"description": "正则表达式模式,用于 extract 操作。常用预设: email(邮箱), phone(电话), url(网址)",
},
},
"required": []string{"action", "text"},
},
}
}
// Execute performs text processing operations.
func (t *TextTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) {
action, ok := arguments["action"].(string)
if !ok || action == "" {
return &ToolResult{
ToolName: "text",
Success: false,
Error: "缺少 action 参数",
}, nil
}
text, ok := arguments["text"].(string)
if !ok || strings.TrimSpace(text) == "" {
return &ToolResult{
ToolName: "text",
Success: false,
Error: "缺少 text 参数或文本为空",
}, nil
}
switch action {
case "count":
return t.handleCount(text)
case "summarize":
return t.handleSummarize(text)
case "translate":
return t.handleTranslate(arguments)
case "extract":
return t.handleExtract(arguments)
default:
return &ToolResult{
ToolName: "text",
Success: false,
Error: fmt.Sprintf("未知操作: %s,支持: count, summarize, translate, extract", action),
}, nil
}
}
// handleCount counts characters, words, lines, and paragraphs in the text.
func (t *TextTool) handleCount(text string) (*ToolResult, error) {
charCount := len([]rune(text))
byteCount := len(text)
words := strings.Fields(text)
wordCount := len(words)
lines := strings.Split(text, "\n")
lineCount := len(lines)
// Count paragraphs (separated by double newlines)
paragraphs := regexp.MustCompile(`\n\s*\n`).Split(text, -1)
paraCount := 0
for _, p := range paragraphs {
if strings.TrimSpace(p) != "" {
paraCount++
}
}
// Count Chinese characters
chineseCount := 0
for _, r := range text {
if unicode.Is(unicode.Han, r) {
chineseCount++
}
}
return &ToolResult{
ToolName: "text",
Success: true,
Data: fmt.Sprintf("文本统计结果:\n- 字符数 (含空格): %d\n- 字符数 (不含空格): %d\n- 字节数: %d\n- 单词数: %d\n- 行数: %d\n- 段落数: %d\n- 中文字符数: %d",
charCount, len([]rune(strings.ReplaceAll(text, " ", ""))),
byteCount, wordCount, lineCount, paraCount, chineseCount),
}, nil
}
// handleSummarize generates a simple summary by extracting the first paragraph and key sentences.
func (t *TextTool) handleSummarize(text string) (*ToolResult, error) {
var result strings.Builder
result.WriteString("文本摘要:\n\n")
// Extract first paragraph
paragraphs := regexp.MustCompile(`\n\s*\n`).Split(text, -1)
var firstPara string
for _, p := range paragraphs {
if trimmed := strings.TrimSpace(p); trimmed != "" {
firstPara = trimmed
break
}
}
if firstPara != "" {
result.WriteString("【首段】\n")
// Truncate if very long
runes := []rune(firstPara)
if len(runes) > 300 {
firstPara = string(runes[:300]) + "..."
}
result.WriteString(firstPara)
result.WriteString("\n\n")
}
// Extract key sentences (longer sentences with important keywords)
sentences := t.splitSentences(text)
keySentences := t.extractKeySentences(sentences, 5)
if len(keySentences) > 0 {
result.WriteString("【关键句】\n")
for i, s := range keySentences {
result.WriteString(fmt.Sprintf("%d. %s\n", i+1, s))
}
}
// Overall stats
lines := strings.Split(text, "\n")
words := strings.Fields(text)
result.WriteString(fmt.Sprintf("\n【概况】共 %d 段、%d 句、%d 词、%d 行",
len(paragraphs), len(sentences), len(words), len(lines)))
return &ToolResult{
ToolName: "text",
Success: true,
Data: result.String(),
}, nil
}
// splitSentences splits text into sentences based on punctuation.
func (t *TextTool) splitSentences(text string) []string {
re := regexp.MustCompile(`[^。!?.!?\n]+[。!?.!?\n]?`)
return re.FindAllString(text, -1)
}
// extractKeySentences selects the most informative sentences (longer ones with keyword hints).
func (t *TextTool) extractKeySentences(sentences []string, maxCount int) []string {
type scored struct {
text string
score int
}
var scoredList []scored
keywords := []string{"重要", "关键", "核心", "主要", "首先", "最后", "因此", "所以", "总结",
"important", "key", "critical", "significant", "therefore", "conclusion", "summary"}
for _, s := range sentences {
trimmed := strings.TrimSpace(s)
if len([]rune(trimmed)) < 10 {
continue
}
score := len([]rune(trimmed)) // longer sentences are more likely informative
lower := strings.ToLower(trimmed)
for _, kw := range keywords {
if strings.Contains(lower, kw) {
score += 50
}
}
scoredList = append(scoredList, scored{text: trimmed, score: score})
}
// Sort by score descending (simple bubble sort for small lists)
for i := 0; i < len(scoredList); i++ {
for j := i + 1; j < len(scoredList); j++ {
if scoredList[j].score > scoredList[i].score {
scoredList[i], scoredList[j] = scoredList[j], scoredList[i]
}
}
}
result := make([]string, 0, maxCount)
for i := 0; i < len(scoredList) && i < maxCount; i++ {
result = append(result, scoredList[i].text)
}
return result
}
// handleTranslate provides a translation placeholder (actual translation requires LLM).
func (t *TextTool) handleTranslate(arguments map[string]interface{}) (*ToolResult, error) {
text, _ := arguments["text"].(string)
targetLang, _ := arguments["target_lang"].(string)
if targetLang == "" {
targetLang = "zh"
}
langNames := map[string]string{
"en": "英语",
"zh": "中文",
"ja": "日语",
"ko": "韩语",
"fr": "法语",
"de": "德语",
}
langName, ok := langNames[targetLang]
if !ok {
langName = targetLang
}
return &ToolResult{
ToolName: "text",
Success: true,
Data: fmt.Sprintf("【翻译请求】\n目标语言: %s (%s)\n原文 (%d 字符):\n---\n%s\n---\n\n提示: 实际翻译由LLM完成,请基于以上原文和目标语言进行翻译。",
langName, targetLang, len([]rune(text)), text),
}, nil
}
// handleExtract extracts patterns like emails, phones, URLs from text using regex.
func (t *TextTool) handleExtract(arguments map[string]interface{}) (*ToolResult, error) {
text, _ := arguments["text"].(string)
pattern, _ := arguments["pattern"].(string)
// Predefined patterns
presets := map[string]string{
"email": `[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`,
"phone": `(?:\+?86[\-\s]?)?1[3-9]\d{9}`,
"url": `https?://[^\s<>"{}|\\^` + "`" + `\[\]]+`,
}
if preset, ok := presets[strings.ToLower(pattern)]; ok {
pattern = preset
}
if pattern == "" {
// Extract all common patterns when no specific pattern given
var result strings.Builder
result.WriteString("文本提取结果:\n\n")
for name, p := range presets {
re, err := regexp.Compile(p)
if err != nil {
continue
}
matches := re.FindAllString(text, -1)
if len(matches) > 0 {
result.WriteString(fmt.Sprintf("【%s】(共 %d 个):\n", name, len(matches)))
seen := make(map[string]bool)
for _, m := range matches {
if !seen[m] {
result.WriteString(fmt.Sprintf(" - %s\n", m))
seen[m] = true
}
}
result.WriteString("\n")
}
}
if result.Len() == len("文本提取结果:\n\n") {
return &ToolResult{
ToolName: "text",
Success: true,
Data: "未提取到匹配的内容(邮箱、电话、URL)",
}, nil
}
return &ToolResult{
ToolName: "text",
Success: true,
Data: result.String(),
}, nil
}
// Use custom regex pattern
re, err := regexp.Compile(pattern)
if err != nil {
return &ToolResult{
ToolName: "text",
Success: false,
Error: fmt.Sprintf("正则表达式无效: %v", err),
}, nil
}
matches := re.FindAllString(text, -1)
if len(matches) == 0 {
return &ToolResult{
ToolName: "text",
Success: true,
Data: fmt.Sprintf("未找到匹配模式 '%s' 的内容", pattern),
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("正则提取结果 (模式: %s, 共 %d 个匹配):\n", pattern, len(matches)))
seen := make(map[string]bool)
for _, m := range matches {
if !seen[m] {
result.WriteString(fmt.Sprintf(" - %s\n", m))
seen[m] = true
}
}
return &ToolResult{
ToolName: "text",
Success: true,
Data: result.String(),
}, nil
}