Refactored orchestrator for staged file handling, added structured prompt support, adjusted Feishu file handling

This commit is contained in:
whlaoding
2026-03-08 22:38:29 +08:00
parent e2f806edb3
commit 52b8dbb835
30 changed files with 9325 additions and 34 deletions

149
tools/git/git.go Normal file
View File

@@ -0,0 +1,149 @@
package git
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
"laodingbot/internal/logger"
)
// Tool provides common Git operations through a safe command whitelist.
type Tool struct {
repoDir string
allowedSubcmds map[string]struct{}
timeout time.Duration
maxOutputChars int
log *logger.Logger
}
// New creates a new git tool.
func New(repoDir string, timeout time.Duration, maxOutputChars int, log *logger.Logger) *Tool {
absRepo, err := filepath.Abs(strings.TrimSpace(repoDir))
if err != nil {
absRepo = strings.TrimSpace(repoDir)
}
if timeout <= 0 {
timeout = 20 * time.Second
}
if maxOutputChars <= 0 {
maxOutputChars = 4000
}
allowed := map[string]struct{}{
"status": {},
"log": {},
"show": {},
"diff": {},
"branch": {},
"checkout": {},
"switch": {},
"restore": {},
"add": {},
"commit": {},
"reset": {},
"revert": {},
"merge": {},
"rebase": {},
"cherry-pick": {},
"fetch": {},
"pull": {},
"push": {},
"remote": {},
"tag": {},
"stash": {},
"blame": {},
"rev-parse": {},
}
if log != nil {
log.Infof("git tool initialized repo_dir=%s timeout=%s max_output_chars=%d", absRepo, timeout, maxOutputChars)
}
return &Tool{
repoDir: absRepo,
allowedSubcmds: allowed,
timeout: timeout,
maxOutputChars: maxOutputChars,
log: log,
}
}
func (t *Tool) Name() string { return "git" }
func (t *Tool) Description() string {
return "Run common git commands inside repository. Input examples: status --short | log --oneline -n 10 | add . | commit -m fix | branch"
}
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
trimmed := strings.TrimSpace(input)
if trimmed == "" {
if t.log != nil {
t.log.Warnf("git tool rejected empty command")
}
return "", fmt.Errorf("empty git command")
}
parts := strings.Fields(trimmed)
if len(parts) == 0 {
return "", fmt.Errorf("empty git command")
}
if strings.EqualFold(parts[0], "git") {
parts = parts[1:]
}
if len(parts) == 0 {
return "", fmt.Errorf("missing git subcommand")
}
subcmd := strings.ToLower(parts[0])
if _, ok := t.allowedSubcmds[subcmd]; !ok {
if t.log != nil {
t.log.Warnf("git tool rejected subcommand=%s", subcmd)
}
return "", fmt.Errorf("unsupported git subcommand: %s", subcmd)
}
runCtx, cancel := context.WithTimeout(ctx, t.timeout)
defer cancel()
args := append([]string{subcmd}, parts[1:]...)
if t.log != nil {
t.log.Infof("git command start subcommand=%s args=%d full=%q", subcmd, len(parts)-1, strings.Join(args, " "))
}
cmd := exec.CommandContext(runCtx, "git", args...)
cmd.Dir = t.repoDir
cmd.Env = append(cmd.Environ(),
"GIT_TERMINAL_PROMPT=0",
"GIT_PAGER=cat",
"GIT_EDITOR=true",
)
out, err := cmd.CombinedOutput()
outText := strings.TrimSpace(string(out))
if len(outText) > t.maxOutputChars {
outText = outText[:t.maxOutputChars]
}
if err != nil {
if t.log != nil {
t.log.Errorf("git command failed subcommand=%s err=%v output=%q", subcmd, err, outText)
}
if outText == "" {
return "", err
}
return outText, err
}
if t.log != nil {
t.log.Infof("git command success subcommand=%s output_bytes=%d", subcmd, len(out))
}
if outText == "" {
return "ok", nil
}
return outText, nil
}