feat(tools/basic): 内置基础工具(calculator(注入安全)/datetime/random/text/json/markdown)+ 批量注册
This commit is contained in:
1 parent
b3c61aa1ff
commit
08223c06a5
19 files changed
+2074
No files matched your search
@@ -0,0 +1,193 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// markdown exposes Markdown building and parsing helpers: tables, lists, headings
|
||||
// and YAML front-matter metadata.
|
||||
type markdown struct{}
|
||||
|
||||
const (
|
||||
// minHeadingLevel and maxHeadingLevel bound the supported ATX heading depth.
|
||||
minHeadingLevel = 1
|
||||
maxHeadingLevel = 6
|
||||
)
|
||||
|
||||
// Name returns the tool identifier.
|
||||
func (markdown) Name() string { return "markdown" }
|
||||
|
||||
// Description explains what the tool does for the model.
|
||||
func (markdown) Description() string {
|
||||
return "Build or parse Markdown: tables, lists, headings and YAML front-matter metadata."
|
||||
}
|
||||
|
||||
// Parameters returns the tool input JSON Schema. Operation-specific inputs are
|
||||
// validated by Execute.
|
||||
func (markdown) Parameters() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"operation": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []any{"table", "list", "heading", "metadata"},
|
||||
"description": "the operation to perform",
|
||||
},
|
||||
"content": map[string]any{"type": "string", "description": "Markdown body for metadata parsing"},
|
||||
"headers": map[string]any{"type": "array", "description": "table column headers"},
|
||||
"rows": map[string]any{"type": "array", "description": "table rows (array of arrays of strings)"},
|
||||
"items": map[string]any{"type": "array", "description": "list entries"},
|
||||
"ordered": map[string]any{"type": "boolean", "description": "render a numbered list when true"},
|
||||
"level": map[string]any{"type": "integer", "description": "heading level 1..6"},
|
||||
"text": map[string]any{"type": "string", "description": "heading text"},
|
||||
},
|
||||
"required": []any{"operation"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute runs the requested operation.
|
||||
func (markdown) Execute(ctx context.Context, args any) (any, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := asMap(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op, err := reqString(m, "operation")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(op)) {
|
||||
case "table":
|
||||
return mdTable(m)
|
||||
case "list":
|
||||
return mdList(m)
|
||||
case "heading":
|
||||
return mdHeading(m)
|
||||
case "metadata":
|
||||
return mdMeta(m)
|
||||
default:
|
||||
return nil, fmt.Errorf("basic: unknown markdown operation %q: %w", op, ErrUnsupported)
|
||||
}
|
||||
}
|
||||
|
||||
// mdTable builds a Markdown table from headers and rows.
|
||||
func mdTable(m map[string]any) (any, error) {
|
||||
headers, err := strSliceArg(m, "headers")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := strSliceSliceArg(m, "rows")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b strings.Builder
|
||||
write := func(cells []string) {
|
||||
b.WriteString("| ")
|
||||
for i, c := range cells {
|
||||
if i > 0 {
|
||||
b.WriteString(" | ")
|
||||
}
|
||||
b.WriteString(escapeCell(c))
|
||||
}
|
||||
b.WriteString(" |\n")
|
||||
}
|
||||
write(headers)
|
||||
b.WriteString(tableSeparator(headers))
|
||||
for _, r := range rows {
|
||||
write(r)
|
||||
}
|
||||
return map[string]any{"result": b.String()}, nil
|
||||
}
|
||||
|
||||
// tableSeparator returns the | --- | --- | header separator row for the column count.
|
||||
func tableSeparator(cols []string) string {
|
||||
parts := make([]string, len(cols))
|
||||
for i := range parts {
|
||||
parts[i] = "---"
|
||||
}
|
||||
return "| " + strings.Join(parts, " | ") + " |\n"
|
||||
}
|
||||
|
||||
// escapeCell neutralizes a cell so a pipe inside the value does not break the table.
|
||||
func escapeCell(s string) string {
|
||||
s = strings.ReplaceAll(s, "|", `\|`)
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
return strings.ReplaceAll(s, "\r", "")
|
||||
}
|
||||
|
||||
// mdList builds a bullet or numbered list from items.
|
||||
func mdList(m map[string]any) (any, error) {
|
||||
items, err := strSliceArg(m, "items")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ordered, err := optBool(m, "ordered", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b strings.Builder
|
||||
for i, it := range items {
|
||||
if ordered {
|
||||
fmt.Fprintf(&b, "%d. %s\n", i+1, it)
|
||||
} else {
|
||||
b.WriteString("- " + it + "\n")
|
||||
}
|
||||
}
|
||||
return map[string]any{"result": b.String()}, nil
|
||||
}
|
||||
|
||||
// mdHeading builds a Markdown ATX heading.
|
||||
func mdHeading(m map[string]any) (any, error) {
|
||||
level, err := optInt(m, "level", 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text, err := reqString(m, "text")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if level < minHeadingLevel || level > maxHeadingLevel {
|
||||
return nil, fmt.Errorf("basic: heading level must be between %d and %d: %w", minHeadingLevel, maxHeadingLevel, ErrInvalidArgument)
|
||||
}
|
||||
return map[string]any{"result": strings.Repeat("#", level) + " " + text + "\n"}, nil
|
||||
}
|
||||
|
||||
// mdMeta parses YAML front matter from content and returns the metadata table plus
|
||||
// the remaining body.
|
||||
func mdMeta(m map[string]any) (any, error) {
|
||||
content, err := reqString(m, "content")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, body := parseFrontMatter(content)
|
||||
return map[string]any{"meta": meta, "body": body}, nil
|
||||
}
|
||||
|
||||
// parseFrontMatter splits leading `--- ... ---` front matter from a Markdown body.
|
||||
// When the document has no front matter it returns an empty table and the whole
|
||||
// content as the body.
|
||||
func parseFrontMatter(content string) (map[string]string, string) {
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
||||
return map[string]string{}, content
|
||||
}
|
||||
meta := map[string]string{}
|
||||
i := 1
|
||||
for i < len(lines) {
|
||||
line := strings.TrimRight(lines[i], "\r")
|
||||
if strings.TrimSpace(line) == "---" {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if key, val, ok := strings.Cut(line, ":"); ok {
|
||||
meta[strings.TrimSpace(key)] = strings.TrimSpace(val)
|
||||
}
|
||||
i++
|
||||
}
|
||||
body := strings.Join(lines[i:], "\n")
|
||||
return meta, body
|
||||
}
|
||||
Reference in new issue
Block a user