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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: 转换为HTML;to_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 
|
||||
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  - 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 v4;password: 生成安全密码;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]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user