From 5150a23256808925656eabc8ed119fc7ad710436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Tue, 9 Jun 2026 13:48:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.go | 472 ++++++++++++++++++++++++++++++++++++++------ templates/chat.html | 58 +++++- 2 files changed, 464 insertions(+), 66 deletions(-) diff --git a/main.go b/main.go index 02ce48b..eead515 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,9 @@ import ( const ( defaultOpenAIBaseURL = "https://ark.cn-beijing.volces.com/api/v3" defaultOpenAITimeout = 120 + defaultSearchBaseURL = "https://api.duckduckgo.com/" + defaultSearchTimeout = 10 + defaultSearchCount = 5 ) type OpenAIConfig struct { @@ -43,6 +46,45 @@ type OpenAIConfig struct { type OpenAIConfigs []OpenAIConfig +type SearchConfig struct { + Name string `yaml:"name" json:"name"` + Active bool `yaml:"active,omitempty" json:"active"` + Enabled bool `yaml:"enabled" json:"enabled"` + Provider string `yaml:"provider" json:"provider"` + APIKey string `yaml:"api_key" json:"-"` + BaseURL string `yaml:"base_url" json:"base_url"` + Count int `yaml:"count" json:"count"` + Timeout int `yaml:"timeout" json:"timeout"` +} + +type SearchConfigs []SearchConfig + +func (configs *SearchConfigs) UnmarshalYAML(value *yaml.Node) error { + switch value.Kind { + case yaml.SequenceNode: + var items []SearchConfig + if err := value.Decode(&items); err != nil { + return err + } + *configs = items + case yaml.MappingNode: + var item SearchConfig + if err := value.Decode(&item); err != nil { + return err + } + *configs = []SearchConfig{item} + case yaml.ScalarNode: + if value.Tag == "!!null" { + *configs = nil + return nil + } + return fmt.Errorf("search 配置格式无效") + default: + return fmt.Errorf("search 配置格式无效") + } + return nil +} + func (configs *OpenAIConfigs) UnmarshalYAML(value *yaml.Node) error { switch value.Kind { case yaml.SequenceNode: @@ -75,14 +117,7 @@ type Config struct { Address string `yaml:"address"` } `yaml:"server"` OpenAI OpenAIConfigs `yaml:"openai"` - Search struct { - Enabled bool `yaml:"enabled"` - Provider string `yaml:"provider"` - APIKey string `yaml:"api_key"` - BaseURL string `yaml:"base_url"` - Count int `yaml:"count"` - Timeout int `yaml:"timeout"` - } `yaml:"search"` + Search SearchConfigs `yaml:"search"` } func defaultOpenAIConfig() OpenAIConfig { @@ -94,15 +129,24 @@ func defaultOpenAIConfig() OpenAIConfig { } } +func defaultSearchConfig() SearchConfig { + return SearchConfig{ + Name: "duckduckgo", + Active: true, + Enabled: true, + Provider: "duckduckgo", + BaseURL: defaultSearchBaseURL, + Count: defaultSearchCount, + Timeout: defaultSearchTimeout, + } +} + func defaultConfig() Config { var cfg Config cfg.Server.Mode = "tcp" cfg.Server.Address = "0.0.0.0:8080" cfg.OpenAI = OpenAIConfigs{defaultOpenAIConfig()} - cfg.Search.Provider = "brave" - cfg.Search.BaseURL = "https://api.search.brave.com/res/v1/web/search" - cfg.Search.Count = 5 - cfg.Search.Timeout = 10 + cfg.Search = SearchConfigs{defaultSearchConfig()} return cfg } @@ -128,8 +172,15 @@ func loadConfig(path string) (*Config, error) { cfg.OpenAI[i].APIKey = key } } + if _, err := normalizeSearchConfigs(&cfg); err != nil { + return nil, err + } if key := os.Getenv("BRAVE_SEARCH_API_KEY"); key != "" { - cfg.Search.APIKey = key + for i := range cfg.Search { + if strings.ToLower(cfg.Search[i].Provider) == "brave" { + cfg.Search[i].APIKey = key + } + } } return &cfg, nil } @@ -181,35 +232,13 @@ func ensureConfigFile(path string) error { changed = true } - search, _ := raw["search"].(map[string]any) - if search == nil { - cfg.Search = defaults.Search + if _, ok := raw["search"].([]any); !ok { + changed = true + } + if normalized, err := normalizeSearchConfigs(&cfg); err != nil { + return err + } else if normalized { changed = true - } else { - if _, ok := search["enabled"]; !ok { - cfg.Search.Enabled = defaults.Search.Enabled - changed = true - } - if _, ok := search["provider"]; !ok { - cfg.Search.Provider = defaults.Search.Provider - changed = true - } - if _, ok := search["api_key"]; !ok { - cfg.Search.APIKey = defaults.Search.APIKey - changed = true - } - if _, ok := search["base_url"]; !ok { - cfg.Search.BaseURL = defaults.Search.BaseURL - changed = true - } - if _, ok := search["count"]; !ok { - cfg.Search.Count = defaults.Search.Count - changed = true - } - if _, ok := search["timeout"]; !ok { - cfg.Search.Timeout = defaults.Search.Timeout - changed = true - } } if !changed { @@ -270,6 +299,74 @@ func normalizeOpenAIConfigs(cfg *Config) (bool, error) { return changed, nil } +func normalizeSearchConfigs(cfg *Config) (bool, error) { + changed := false + if len(cfg.Search) == 0 { + cfg.Search = SearchConfigs{defaultSearchConfig()} + changed = true + } + + activeIndex := -1 + seen := map[string]bool{} + for i := range cfg.Search { + profile := &cfg.Search[i] + profile.Provider = strings.ToLower(strings.TrimSpace(profile.Provider)) + if profile.Provider == "" { + profile.Provider = "duckduckgo" + changed = true + } + name := strings.TrimSpace(profile.Name) + if name == "" { + name = profile.Provider + if seen[name] { + name = fmt.Sprintf("%s-%d", profile.Provider, i+1) + } + profile.Name = name + changed = true + } else if name != profile.Name { + profile.Name = name + changed = true + } + if seen[name] { + return changed, fmt.Errorf("search 配置名称重复: %s", name) + } + seen[name] = true + + if strings.TrimSpace(profile.BaseURL) == "" { + switch profile.Provider { + case "duckduckgo", "ddg": + profile.BaseURL = defaultSearchBaseURL + case "brave": + profile.BaseURL = "https://api.search.brave.com/res/v1/web/search" + default: + return changed, fmt.Errorf("暂不支持搜索服务: %s", profile.Provider) + } + changed = true + } + if profile.Count <= 0 { + profile.Count = defaultSearchCount + changed = true + } + if profile.Timeout <= 0 { + profile.Timeout = defaultSearchTimeout + changed = true + } + if profile.Active { + if activeIndex == -1 { + activeIndex = i + } else { + profile.Active = false + changed = true + } + } + } + if activeIndex == -1 { + cfg.Search[0].Active = true + changed = true + } + return changed, nil +} + func writeConfig(path string, cfg Config) error { data, err := yaml.Marshal(&cfg) if err != nil { @@ -323,7 +420,7 @@ type OpenAIState struct { activeName string } -type openAIActiveRequest struct { +type activeProfileRequest struct { Name string `json:"name"` } @@ -332,6 +429,11 @@ type openAIListResponse struct { Profiles []OpenAIConfig `json:"profiles"` } +type searchListResponse struct { + Active string `json:"active"` + Profiles []SearchConfig `json:"profiles"` +} + func NewOpenAIState(configs []OpenAIConfig) (*OpenAIState, error) { state := &OpenAIState{ profiles: make(map[string]*OpenAIProfile, len(configs)), @@ -433,12 +535,96 @@ func publicOpenAIConfig(profile *OpenAIProfile, active bool) OpenAIConfig { return config } +type SearchState struct { + mu sync.RWMutex + profiles map[string]SearchConfig + order []string + activeName string +} + +func NewSearchState(configs []SearchConfig) (*SearchState, error) { + state := &SearchState{ + profiles: make(map[string]SearchConfig, len(configs)), + order: make([]string, 0, len(configs)), + } + for _, config := range configs { + if strings.TrimSpace(config.Name) == "" { + return nil, errors.New("search.name 不能为空") + } + if strings.TrimSpace(config.Provider) == "" { + return nil, fmt.Errorf("search.%s.provider 未配置", config.Name) + } + if strings.TrimSpace(config.BaseURL) == "" { + return nil, fmt.Errorf("search.%s.base_url 未配置", config.Name) + } + if config.Timeout <= 0 { + return nil, fmt.Errorf("search.%s.timeout 必须大于 0", config.Name) + } + if _, ok := state.profiles[config.Name]; ok { + return nil, fmt.Errorf("search 配置名称重复: %s", config.Name) + } + state.profiles[config.Name] = config + state.order = append(state.order, config.Name) + if config.Active && state.activeName == "" { + state.activeName = config.Name + } + } + if len(state.order) == 0 { + return nil, errors.New("search 配置不能为空") + } + if state.activeName == "" { + state.activeName = state.order[0] + } + return state, nil +} + +func (s *SearchState) ActiveProfile() SearchConfig { + s.mu.RLock() + defer s.mu.RUnlock() + return s.profiles[s.activeName] +} + +func (s *SearchState) SwitchActive(name string) (SearchConfig, error) { + name = strings.TrimSpace(name) + if name == "" { + return SearchConfig{}, errors.New("搜索配置名称不能为空") + } + s.mu.Lock() + defer s.mu.Unlock() + profile, ok := s.profiles[name] + if !ok { + return SearchConfig{}, fmt.Errorf("搜索配置不存在: %s", name) + } + s.activeName = name + return profile, nil +} + +func (s *SearchState) ListProfiles() searchListResponse { + s.mu.RLock() + defer s.mu.RUnlock() + profiles := make([]SearchConfig, 0, len(s.order)) + for _, name := range s.order { + profile := s.profiles[name] + profile.APIKey = "" + profile.Active = name == s.activeName + profiles = append(profiles, profile) + } + return searchListResponse{Active: s.activeName, Profiles: profiles} +} + +func publicSearchConfig(config SearchConfig, active bool) SearchConfig { + config.APIKey = "" + config.Active = active + return config +} + // ─── 全局变量 ───────────────────────────────────────────── var ( - cfg *Config - aiState *OpenAIState - store *ConvStore + cfg *Config + aiState *OpenAIState + searchState *SearchState + store *ConvStore ) // ─── 路由 ───────────────────────────────────────────────── @@ -457,7 +643,7 @@ func listOpenAIHandler(c *gin.Context) { } func switchOpenAIHandler(c *gin.Context) { - var req openAIActiveRequest + var req activeProfileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误: " + err.Error()}) return @@ -473,6 +659,27 @@ func switchOpenAIHandler(c *gin.Context) { }) } +func listSearchHandler(c *gin.Context) { + c.JSON(http.StatusOK, searchState.ListProfiles()) +} + +func switchSearchHandler(c *gin.Context) { + var req activeProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式错误: " + err.Error()}) + return + } + profile, err := searchState.SwitchActive(req.Name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{ + "active": profile.Name, + "profile": publicSearchConfig(profile, true), + }) +} + func listConversationsHandler(c *gin.Context) { convs, err := store.List() if err != nil { @@ -619,23 +826,39 @@ type braveSearchResponse struct { } `json:"web"` } +type duckDuckGoResponse struct { + Abstract string `json:"Abstract"` + AbstractSource string `json:"AbstractSource"` + AbstractURL string `json:"AbstractURL"` + Heading string `json:"Heading"` + RelatedTopics []struct { + Text string `json:"Text"` + FirstURL string `json:"FirstURL"` + Topics []struct { + Text string `json:"Text"` + FirstURL string `json:"FirstURL"` + } `json:"Topics"` + } `json:"RelatedTopics"` + Infobox struct { + Content []struct { + Label string `json:"label"` + Value string `json:"value"` + } `json:"content"` + } `json:"Infobox"` +} + func enrichMessagesWithSearch(ctx context.Context, messages []ChatMessage) ([]ChatMessage, error) { - if !cfg.Search.Enabled { + searchConfig := searchState.ActiveProfile() + if !searchConfig.Enabled { return nil, errors.New("联网搜索未启用,请先在 config.yaml 中配置 search.enabled") } - if strings.ToLower(cfg.Search.Provider) != "brave" { - return nil, fmt.Errorf("暂不支持搜索服务: %s", cfg.Search.Provider) - } - if cfg.Search.APIKey == "" { - return nil, errors.New("联网搜索未配置 API Key,请设置 search.api_key 或环境变量 BRAVE_SEARCH_API_KEY") - } query := latestUserQuery(messages) if query == "" { return nil, errors.New("联网搜索需要输入文本问题") } - results, err := braveWebSearch(ctx, query) + results, err := webSearch(ctx, searchConfig, query) if err != nil { return nil, err } @@ -643,7 +866,7 @@ func enrichMessagesWithSearch(ctx context.Context, messages []ChatMessage) ([]Ch return nil, errors.New("未搜索到相关网页结果") } - searchContext := buildSearchContext(query, results) + searchContext := buildSearchContext(searchConfig, query, results) withSearch := make([]ChatMessage, 0, len(messages)+1) withSearch = append(withSearch, ChatMessage{Role: "system", Content: searchContext, Hidden: true}) withSearch = append(withSearch, messages...) @@ -659,17 +882,128 @@ func latestUserQuery(messages []ChatMessage) string { return "" } -func braveWebSearch(ctx context.Context, query string) ([]searchResult, error) { - searchCtx, cancel := context.WithTimeout(ctx, time.Duration(cfg.Search.Timeout)*time.Second) +func webSearch(ctx context.Context, config SearchConfig, query string) ([]searchResult, error) { + switch strings.ToLower(config.Provider) { + case "duckduckgo", "ddg": + return duckDuckGoSearch(ctx, config, query) + case "brave": + return braveWebSearch(ctx, config, query) + default: + return nil, fmt.Errorf("暂不支持搜索服务: %s", config.Provider) + } +} + +func duckDuckGoSearch(ctx context.Context, config SearchConfig, query string) ([]searchResult, error) { + searchCtx, cancel := context.WithTimeout(ctx, time.Duration(config.Timeout)*time.Second) defer cancel() - u, err := url.Parse(cfg.Search.BaseURL) + u, err := url.Parse(config.BaseURL) if err != nil { return nil, fmt.Errorf("搜索服务地址无效: %w", err) } q := u.Query() q.Set("q", query) - q.Set("count", fmt.Sprintf("%d", cfg.Search.Count)) + q.Set("format", "json") + q.Set("no_html", "1") + q.Set("skip_disambig", "1") + u.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(searchCtx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("创建搜索请求失败: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "aichat/1.0") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("联网搜索失败: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024)) + if err != nil { + return nil, fmt.Errorf("读取搜索响应失败: %w", err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("搜索服务返回错误 %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + var parsed duckDuckGoResponse + if err := json.Unmarshal(body, &parsed); err != nil { + return nil, fmt.Errorf("解析搜索响应失败: %w", err) + } + + limit := config.Count + if limit <= 0 { + limit = defaultSearchCount + } + results := make([]searchResult, 0, limit) + if strings.TrimSpace(parsed.Abstract) != "" { + title := strings.TrimSpace(parsed.Heading) + if title == "" { + title = strings.TrimSpace(parsed.AbstractSource) + } + if title == "" { + title = "DuckDuckGo 摘要" + } + results = append(results, searchResult{Title: title, URL: strings.TrimSpace(parsed.AbstractURL), Description: strings.TrimSpace(parsed.Abstract)}) + } + for _, item := range parsed.Infobox.Content { + if len(results) >= limit { + break + } + if strings.TrimSpace(item.Label) == "" || strings.TrimSpace(item.Value) == "" { + continue + } + results = append(results, searchResult{Title: item.Label, Description: item.Value}) + } + appendRelated := func(text, firstURL string) { + if len(results) >= limit || strings.TrimSpace(text) == "" { + return + } + title, desc := splitDuckDuckGoText(text) + results = append(results, searchResult{Title: title, URL: strings.TrimSpace(firstURL), Description: desc}) + } + for _, topic := range parsed.RelatedTopics { + appendRelated(topic.Text, topic.FirstURL) + for _, nested := range topic.Topics { + appendRelated(nested.Text, nested.FirstURL) + } + if len(results) >= limit { + break + } + } + return results, nil +} + +func splitDuckDuckGoText(text string) (string, string) { + text = strings.TrimSpace(text) + parts := strings.SplitN(text, " - ", 2) + if len(parts) == 2 { + return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + } + runes := []rune(text) + if len(runes) > 42 { + return string(runes[:42]) + "...", text + } + return text, text +} + +func braveWebSearch(ctx context.Context, config SearchConfig, query string) ([]searchResult, error) { + if config.APIKey == "" { + return nil, errors.New("Brave 搜索未配置 API Key,请设置 search.api_key 或环境变量 BRAVE_SEARCH_API_KEY") + } + searchCtx, cancel := context.WithTimeout(ctx, time.Duration(config.Timeout)*time.Second) + defer cancel() + + u, err := url.Parse(config.BaseURL) + if err != nil { + return nil, fmt.Errorf("搜索服务地址无效: %w", err) + } + q := u.Query() + q.Set("q", query) + q.Set("count", fmt.Sprintf("%d", config.Count)) q.Set("search_lang", "zh-hans") q.Set("country", "CN") u.RawQuery = q.Encode() @@ -679,7 +1013,7 @@ func braveWebSearch(ctx context.Context, query string) ([]searchResult, error) { return nil, fmt.Errorf("创建搜索请求失败: %w", err) } req.Header.Set("Accept", "application/json") - req.Header.Set("X-Subscription-Token", cfg.Search.APIKey) + req.Header.Set("X-Subscription-Token", config.APIKey) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -702,15 +1036,20 @@ func braveWebSearch(ctx context.Context, query string) ([]searchResult, error) { return parsed.Web.Results, nil } -func buildSearchContext(query string, results []searchResult) string { +func buildSearchContext(config SearchConfig, query string, results []searchResult) string { var b strings.Builder - fmt.Fprintf(&b, "用户开启了联网搜索。请优先根据以下网页搜索结果回答,并在合适位置标注来源链接。\n") + fmt.Fprintf(&b, "用户开启了联网搜索。当前搜索源: %s(%s)。请优先根据以下搜索结果回答,并在合适位置标注来源链接。\n", config.Name, config.Provider) + if strings.ToLower(config.Provider) == "duckduckgo" || strings.ToLower(config.Provider) == "ddg" { + fmt.Fprintln(&b, "注意:DuckDuckGo 即时答案不是全量网页搜索,结果可能较少。") + } fmt.Fprintf(&b, "搜索时间: %s\n", time.Now().Format("2006-01-02 15:04:05")) fmt.Fprintf(&b, "搜索词: %s\n\n", query) fmt.Fprintln(&b, "搜索结果:") for i, r := range results { fmt.Fprintf(&b, "%d. 标题: %s\n", i+1, strings.TrimSpace(r.Title)) - fmt.Fprintf(&b, " 链接: %s\n", strings.TrimSpace(r.URL)) + if strings.TrimSpace(r.URL) != "" { + fmt.Fprintf(&b, " 链接: %s\n", strings.TrimSpace(r.URL)) + } if strings.TrimSpace(r.Description) != "" { fmt.Fprintf(&b, " 摘要: %s\n", strings.TrimSpace(r.Description)) } @@ -1019,6 +1358,11 @@ func main() { fmt.Fprintln(os.Stderr, "OpenAI 配置初始化失败:", err) os.Exit(1) } + searchState, err = NewSearchState(cfg.Search) + if err != nil { + fmt.Fprintln(os.Stderr, "搜索配置初始化失败:", err) + os.Exit(1) + } store = NewConvStore("conversations") // Gin 路由 @@ -1030,6 +1374,8 @@ func main() { r.POST("/api/chat", chatHandler) r.GET("/api/openai", listOpenAIHandler) r.POST("/api/openai/active", switchOpenAIHandler) + r.GET("/api/search", listSearchHandler) + r.POST("/api/search/active", switchSearchHandler) r.GET("/api/conversations", listConversationsHandler) r.POST("/api/conversations", createConversationHandler) r.GET("/api/conversations/:id", getConversationHandler) diff --git a/templates/chat.html b/templates/chat.html index bf1ba1b..bcad0a2 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -144,12 +144,12 @@ padding: 3px 10px; border-radius: 20px; } - header .model-select { + header .profile-select { max-width: 260px; cursor: pointer; outline: none; } - header .model-select:disabled { + header .profile-select:disabled { opacity: .65; cursor: not-allowed; } @@ -475,9 +475,10 @@ {{ .Title }}
- + @@ -544,6 +545,8 @@ let pending = false; let webSearchEnabled = false; let openAIProfiles = []; let activeOpenAIName = '{{ .OpenAIName }}'; +let searchProfiles = []; +let activeSearchName = ''; let imageB64 = ''; // 当前待发送图片的 data URI let imageName = ''; @@ -556,6 +559,7 @@ const btnClear = document.getElementById('btnClear'); const btnPreset = document.getElementById('btnPreset'); const btnSearch = document.getElementById('btnSearch'); const modelSelect = document.getElementById('modelSelect'); +const searchSelect = document.getElementById('searchSelect'); const btnNewChat = document.getElementById('btnNewChat'); const convList = document.getElementById('convList'); const presetModal = document.getElementById('presetModal'); @@ -630,6 +634,7 @@ function setInputDisabled(disabled) { fileInput.disabled = disabled; btnSearch.disabled = disabled; modelSelect.disabled = disabled || openAIProfiles.length <= 1; + searchSelect.disabled = disabled || searchProfiles.length <= 1; } function updateSearchButton() { @@ -658,6 +663,27 @@ async function loadOpenAIProfiles() { modelSelect.disabled = pending || openAIProfiles.length <= 1; } +async function loadSearchProfiles() { + const res = await fetch('/api/search'); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: '加载搜索配置失败' })); + throw new Error(err.error || '加载搜索配置失败'); + } + const data = await res.json(); + searchProfiles = Array.isArray(data.profiles) ? data.profiles : []; + activeSearchName = data.active || ''; + + searchSelect.innerHTML = ''; + for (const profile of searchProfiles) { + const opt = document.createElement('option'); + opt.value = profile.name; + opt.textContent = `${profile.name} · ${profile.provider}`; + opt.selected = profile.name === activeSearchName; + searchSelect.appendChild(opt); + } + searchSelect.disabled = pending || searchProfiles.length <= 1; +} + // ── 对话列表 ────────────────────────────────────────────── async function loadConversationList() { try { @@ -1063,6 +1089,31 @@ modelSelect.addEventListener('change', async () => { alert(e.message); } }); +searchSelect.addEventListener('change', async () => { + if (pending) { + searchSelect.value = activeSearchName; + return; + } + const nextName = searchSelect.value; + const prevName = activeSearchName; + try { + const res = await fetch('/api/search/active', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: nextName }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: '切换搜索源失败' })); + throw new Error(err.error || '切换搜索源失败'); + } + const data = await res.json(); + activeSearchName = data.active; + searchSelect.value = activeSearchName; + } catch (e) { + searchSelect.value = prevName; + alert(e.message); + } +}); btnNewChat.addEventListener('click', newConversation); btnClear.addEventListener('click', newConversation); btnPreset.addEventListener('click', openPresetModal); @@ -1083,6 +1134,7 @@ presetModal.addEventListener('click', e => { // 自动聚焦 & 初始化 updateSearchButton(); loadOpenAIProfiles().catch(e => alert(e.message)); +loadSearchProfiles().catch(e => alert(e.message)); loadConversationList(); inputBox.focus();