feat(search): add Baidu Qianfan AI Search provider with i18n docs

- Add BaiduSearchConfig struct and register in WebToolsConfig/defaults
- Insert Baidu Search in priority chain: DuckDuckGo > Baidu > GLM Search
- Use perplexityTimeout (30s) — Qianfan is LLM-based
- Fix response parsing: use references[] field per API spec
- Add baidu_search block to config.example.json

docs: sync configuration.md and README Documentation table across all languages

- Complete truncated configuration.md for fr/ja/pt-br/vi/zh: add Spawn
  async flow diagram, Providers table, Model Configuration (all vendors,
  examples, load balancing, migration), Provider Architecture, Scheduled
  Tasks, and Advanced Topics links
- Add Hooks/Steering/SubTurn entries to Documentation table in all 8
  READMEs (en/zh/fr/id/it/ja/pt-br/vi), ordered before Troubleshooting
- Add Baidu Search row to web search table in all 8 READMEs and
  tools_configuration.md (en + 5 i18n); zh README reorders search
  engines with China-friendly options first
- Add Matrix channel docs translations (fr/ja/pt-br/vi)
- Add Weixin channel to chat-apps.md and all README Channels tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BeaconCat
2026-03-23 00:51:27 +08:00
parent 4d84bd90cd
commit 60a7098fd3
36 changed files with 5683 additions and 2113 deletions
+15 -11
View File
@@ -174,17 +174,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()})
+15 -7
View File
@@ -792,14 +792,22 @@ type GLMSearchConfig struct {
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"`
}
type BaiduSearchConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BAIDU_ENABLED"`
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BAIDU_API_KEY"`
BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_BAIDU_BASE_URL"`
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BAIDU_MAX_RESULTS"`
}
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,
+6
View File
@@ -475,6 +475,12 @@ func DefaultConfig() *Config {
SearchEngine: "search_std",
MaxResults: 5,
},
BaiduSearch: BaiduSearchConfig{
Enabled: false,
APIKey: "",
BaseURL: "https://qianfan.baidubce.com/v2/ai_search/web_search",
MaxResults: 10,
},
},
Cron: CronToolsConfig{
ToolConfig: ToolConfig{
+117 -22
View File
@@ -613,39 +613,120 @@ 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(resp.Body)
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)
}
var lines []string
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 +777,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 {