Merge branch 'main' into fix/binary-tool-output-handling

# Conflicts:
#	pkg/agent/loop.go
#	pkg/agent/loop_test.go
#	pkg/commands/builtin_test.go
#	pkg/tools/send_file_test.go
This commit is contained in:
afjcjsbx
2026-03-23 13:16:23 +01:00
162 changed files with 14301 additions and 5066 deletions
+4 -3
View File
@@ -133,9 +133,10 @@ func (t *SendFileTool) Execute(ctx context.Context, args map[string]any) *ToolRe
scope := fmt.Sprintf("tool:send_file:%s:%s", channel, chatID)
ref, err := t.mediaStore.Store(resolved, media.MediaMeta{
Filename: filename,
ContentType: mediaType,
Source: "tool:send_file",
Filename: filename,
ContentType: mediaType,
Source: "tool:send_file",
CleanupPolicy: media.CleanupPolicyForgetOnly,
}, scope)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to register media: %v", err))
+8
View File
@@ -107,6 +107,14 @@ func TestSendFileTool_Success(t *testing.T) {
if !result.ResponseHandled {
t.Fatal("expected send_file success to mark response handled")
}
_, meta, err := store.ResolveWithMeta(result.Media[0])
if err != nil {
t.Fatalf("ResolveWithMeta failed: %v", err)
}
if meta.CleanupPolicy != media.CleanupPolicyForgetOnly {
t.Errorf("CleanupPolicy = %q, want %q", meta.CleanupPolicy, media.CleanupPolicyForgetOnly)
}
}
func TestSendFileTool_CustomFilename(t *testing.T) {
+121 -22
View File
@@ -613,39 +613,124 @@ func (p *GLMSearchProvider) Search(ctx context.Context, query string, count int)
return strings.Join(lines, "\n"), nil
}
type BaiduSearchProvider struct {
apiKey string
baseURL string
proxy string
client *http.Client
}
func (p *BaiduSearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
searchURL := p.baseURL
if searchURL == "" {
searchURL = "https://qianfan.baidubce.com/v2/ai_search/web_search"
}
payload := map[string]any{
"messages": []map[string]string{
{
"role": "user",
"content": query,
},
},
"search_source": "baidu_search_v2",
"resource_type_filter": []map[string]any{{"type": "web", "top_k": count}},
}
bodyBytes, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", searchURL, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+p.apiKey)
resp, err := p.client.Do(req)
if err != nil {
return "", fmt.Errorf("baidu search request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("baidu search API error %d: %s", resp.StatusCode, string(body))
}
var result struct {
References []struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
} `json:"references"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("failed to parse response: %w", err)
}
if len(result.References) == 0 {
return fmt.Sprintf("No results for: %s", query), nil
}
lines := []string{fmt.Sprintf("Results for: %s (via Baidu Search)", query)}
for i, item := range result.References {
if i >= count {
break
}
lines = append(lines, fmt.Sprintf("%d. %s\n %s", i+1, item.Title, item.URL))
if item.Content != "" {
lines = append(lines, fmt.Sprintf(" %s", item.Content))
}
}
return strings.Join(lines, "\n"), nil
}
type WebSearchTool struct {
provider SearchProvider
maxResults int
}
type WebSearchToolOptions struct {
BraveAPIKeys []string
BraveMaxResults int
BraveEnabled bool
TavilyAPIKeys []string
TavilyBaseURL string
TavilyMaxResults int
TavilyEnabled bool
DuckDuckGoMaxResults int
DuckDuckGoEnabled bool
PerplexityAPIKeys []string
PerplexityMaxResults int
PerplexityEnabled bool
SearXNGBaseURL string
SearXNGMaxResults int
SearXNGEnabled bool
GLMSearchAPIKey string
GLMSearchBaseURL string
GLMSearchEngine string
GLMSearchMaxResults int
GLMSearchEnabled bool
Proxy string
BraveAPIKeys []string
BraveMaxResults int
BraveEnabled bool
TavilyAPIKeys []string
TavilyBaseURL string
TavilyMaxResults int
TavilyEnabled bool
DuckDuckGoMaxResults int
DuckDuckGoEnabled bool
PerplexityAPIKeys []string
PerplexityMaxResults int
PerplexityEnabled bool
SearXNGBaseURL string
SearXNGMaxResults int
SearXNGEnabled bool
GLMSearchAPIKey string
GLMSearchBaseURL string
GLMSearchEngine string
GLMSearchMaxResults int
GLMSearchEnabled bool
BaiduSearchAPIKey string
BaiduSearchBaseURL string
BaiduSearchMaxResults int
BaiduSearchEnabled bool
Proxy string
}
func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
var provider SearchProvider
maxResults := 5
// Priority: Perplexity > Brave > SearXNG > Tavily > DuckDuckGo > GLM Search
// Priority: Perplexity > Brave > SearXNG > Tavily > DuckDuckGo > Baidu Search > GLM Search
if opts.PerplexityEnabled && len(opts.PerplexityAPIKeys) > 0 {
client, err := utils.CreateHTTPClient(opts.Proxy, perplexityTimeout)
if err != nil {
@@ -696,6 +781,20 @@ func NewWebSearchTool(opts WebSearchToolOptions) (*WebSearchTool, error) {
if opts.DuckDuckGoMaxResults > 0 {
maxResults = opts.DuckDuckGoMaxResults
}
} else if opts.BaiduSearchEnabled && opts.BaiduSearchAPIKey != "" {
client, err := utils.CreateHTTPClient(opts.Proxy, perplexityTimeout)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP client for Baidu Search: %w", err)
}
provider = &BaiduSearchProvider{
apiKey: opts.BaiduSearchAPIKey,
baseURL: opts.BaiduSearchBaseURL,
proxy: opts.Proxy,
client: client,
}
if opts.BaiduSearchMaxResults > 0 {
maxResults = opts.BaiduSearchMaxResults
}
} else if opts.GLMSearchEnabled && opts.GLMSearchAPIKey != "" {
client, err := utils.CreateHTTPClient(opts.Proxy, searchTimeout)
if err != nil {