Merge branch 'main' into version

This commit is contained in:
Cytown
2026-03-23 11:41:36 +08:00
40 changed files with 5736 additions and 2129 deletions
+15 -11
View File
@@ -177,17 +177,21 @@ func registerSharedTools(
cfg.Tools.Web.Perplexity.APIKey(),
cfg.Tools.Web.Perplexity.APIKeys(),
),
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
SearXNGBaseURL: cfg.Tools.Web.SearXNG.BaseURL,
SearXNGMaxResults: cfg.Tools.Web.SearXNG.MaxResults,
SearXNGEnabled: cfg.Tools.Web.SearXNG.Enabled,
GLMSearchAPIKey: cfg.Tools.Web.GLMSearch.APIKey(),
GLMSearchBaseURL: cfg.Tools.Web.GLMSearch.BaseURL,
GLMSearchEngine: cfg.Tools.Web.GLMSearch.SearchEngine,
GLMSearchMaxResults: cfg.Tools.Web.GLMSearch.MaxResults,
GLMSearchEnabled: cfg.Tools.Web.GLMSearch.Enabled,
Proxy: cfg.Tools.Web.Proxy,
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
SearXNGBaseURL: cfg.Tools.Web.SearXNG.BaseURL,
SearXNGMaxResults: cfg.Tools.Web.SearXNG.MaxResults,
SearXNGEnabled: cfg.Tools.Web.SearXNG.Enabled,
GLMSearchAPIKey: cfg.Tools.Web.GLMSearch.APIKey(),
GLMSearchBaseURL: cfg.Tools.Web.GLMSearch.BaseURL,
GLMSearchEngine: cfg.Tools.Web.GLMSearch.SearchEngine,
GLMSearchMaxResults: cfg.Tools.Web.GLMSearch.MaxResults,
GLMSearchEnabled: cfg.Tools.Web.GLMSearch.Enabled,
BaiduSearchAPIKey: cfg.Tools.Web.BaiduSearch.APIKey(),
BaiduSearchBaseURL: cfg.Tools.Web.BaiduSearch.BaseURL,
BaiduSearchMaxResults: cfg.Tools.Web.BaiduSearch.MaxResults,
BaiduSearchEnabled: cfg.Tools.Web.BaiduSearch.Enabled,
Proxy: cfg.Tools.Web.Proxy,
})
if err != nil {
logger.ErrorCF("agent", "Failed to create web search tool", map[string]any{"error": err.Error()})
+36 -7
View File
@@ -1124,14 +1124,33 @@ func (c *GLMSearchConfig) SetAPIKey(key string) {
c.secDirty = true
}
type BaiduSearchConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BAIDU_ENABLED"`
BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_BAIDU_BASE_URL"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BAIDU_MAX_RESULTS"`
apiKey string
secDirty bool
}
// APIKey returns the Baidu search API key
func (c *BaiduSearchConfig) APIKey() string {
return c.apiKey
}
func (c *BaiduSearchConfig) SetAPIKey(key string) {
c.apiKey = key
c.secDirty = true
}
type WebToolsConfig struct {
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"`
Brave BraveConfig ` json:"brave"`
Tavily TavilyConfig ` json:"tavily"`
DuckDuckGo DuckDuckGoConfig ` json:"duckduckgo"`
Perplexity PerplexityConfig ` json:"perplexity"`
SearXNG SearXNGConfig ` json:"searxng"`
GLMSearch GLMSearchConfig ` json:"glm_search"`
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"`
Brave BraveConfig ` json:"brave"`
Tavily TavilyConfig ` json:"tavily"`
DuckDuckGo DuckDuckGoConfig ` json:"duckduckgo"`
Perplexity PerplexityConfig ` json:"perplexity"`
SearXNG SearXNGConfig ` json:"searxng"`
GLMSearch GLMSearchConfig ` json:"glm_search"`
BaiduSearch BaiduSearchConfig ` json:"baidu_search"`
// PreferNative controls whether to use provider-native web search when
// the active LLM supports it (e.g. OpenAI web_search_preview). When true,
// the client-side web_search tool is hidden to avoid duplicate search surfaces,
@@ -1426,6 +1445,10 @@ func applySecurityConfig(cfg *Config, sec *SecurityConfig) error {
cfg.Tools.Web.GLMSearch.apiKey = sec.Web.GLMSearch.APIKey
}
if sec.Web.BaiduSearch != nil && sec.Web.BaiduSearch.APIKey != "" {
cfg.Tools.Web.BaiduSearch.apiKey = sec.Web.BaiduSearch.APIKey
}
if sec.Skills.Github != nil && sec.Skills.Github.Token != "" {
cfg.Tools.Skills.Github.token = sec.Skills.Github.Token
}
@@ -1815,6 +1838,12 @@ func SaveConfig(path string, cfg *Config) error {
}
cfg.Tools.Web.GLMSearch.secDirty = false
}
if cfg.Tools.Web.BaiduSearch.secDirty {
cfg.security.Web.BaiduSearch = &BaiduSearchSecurity{
APIKey: cfg.Tools.Web.BaiduSearch.APIKey(),
}
cfg.Tools.Web.BaiduSearch.secDirty = false
}
if cfg.Tools.Skills.Github.secDirty {
cfg.security.Skills.Github = &GithubSecurity{
Token: cfg.Tools.Skills.Github.Token(),
+5
View File
@@ -419,6 +419,11 @@ func DefaultConfig() *Config {
SearchEngine: "search_std",
MaxResults: 5,
},
BaiduSearch: BaiduSearchConfig{
Enabled: false,
BaseURL: "https://qianfan.baidubce.com/v2/ai_search/web_search",
MaxResults: 10,
},
},
Cron: CronToolsConfig{
ToolConfig: ToolConfig{
+9 -4
View File
@@ -133,10 +133,11 @@ type IRCSecurity struct {
}
type WebToolsSecurity struct {
Brave *BraveSecurity `yaml:"brave,omitempty"`
Tavily *TavilySecurity `yaml:"tavily,omitempty"`
Perplexity *PerplexitySecurity `yaml:"perplexity,omitempty"`
GLMSearch *GLMSearchSecurity `yaml:"glm_search,omitempty"`
Brave *BraveSecurity `yaml:"brave,omitempty"`
Tavily *TavilySecurity `yaml:"tavily,omitempty"`
Perplexity *PerplexitySecurity `yaml:"perplexity,omitempty"`
GLMSearch *GLMSearchSecurity `yaml:"glm_search,omitempty"`
BaiduSearch *BaiduSearchSecurity `yaml:"baidu_search,omitempty"`
}
type BraveSecurity struct {
@@ -155,6 +156,10 @@ type GLMSearchSecurity struct {
APIKey string `yaml:"api_key,omitempty"`
}
type BaiduSearchSecurity struct {
APIKey string `yaml:"api_key,omitempty" env:"PICOCLAW_TOOLS_WEB_BAIDU_API_KEY"`
}
type SkillsSecurity struct {
Github *GithubSecurity `yaml:"github,omitempty"`
ClawHub *ClawHubSecurity `yaml:"clawhub,omitempty"`
+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 {