From 67795cac3d4373f63ac996463fc6afde0587f4c0 Mon Sep 17 00:00:00 2001 From: Color2333 <1552429809@qq.com> Date: Sat, 10 Oct 2026 14:34:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20search-multi=20=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20OpenAlex/DBLP/IEEE=20=E6=B8=A0=E9=81=93=EF=BC=88?= =?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=AB=AF=E4=BA=94=E6=B8=A0=E9=81=93=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=EF=BC=9BbioRxiv=20=E6=98=8E=E7=A1=AE=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=9A=82=E4=B8=8D=E6=94=AF=E6=8C=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/handlers_ingest.go | 4 +- server/phase7.go | 137 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/core/handlers_ingest.go b/core/handlers_ingest.go index cf0e4dc..66504f8 100644 --- a/core/handlers_ingest.go +++ b/core/handlers_ingest.go @@ -193,7 +193,7 @@ func HandleIngestIEEE(ctx context.Context, env *HandlerEnv, task *Task) (map[str if query == "" { return nil, errors.New("缺少 query") } - papers, err := fetchIEEE(ctx, apiKey, query, maxResults) + papers, err := FetchIEEE(ctx, apiKey, query, maxResults) if err != nil { return nil, err } @@ -232,7 +232,7 @@ func HandleIngestIEEE(ctx context.Context, env *HandlerEnv, task *Task) (map[str } // fetchIEEE IEEE Xplore 检索(元数据,无 PDF——权限限制维持原状)。 -func fetchIEEE(ctx context.Context, apiKey, query string, maxResults int) ([]ArxivPaper, error) { +func FetchIEEE(ctx context.Context, apiKey, query string, maxResults int) ([]ArxivPaper, error) { payload := map[string]any{ "queryString": fmt.Sprintf(`("article_title":"%s" OR "abstract":"%s")`, query, query), "maxRecords": maxResults, diff --git a/server/phase7.go b/server/phase7.go index 34eea3e..8c14c21 100644 --- a/server/phase7.go +++ b/server/phase7.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "strings" + "time" core "github.com/Color2333/PaperMind/core" ) @@ -307,6 +308,35 @@ func (s *Server) handleSearchMulti(w http.ResponseWriter, r *http.Request) { if channelEnabled(body.Channels, "semantic_scholar") { channels["semantic_scholar"] = s.searchS2(ctx, body.Query, body.MaxResults) } + // openalex / dblp / ieee + if channelEnabled(body.Channels, "openalex") { + channels["openalex"] = s.searchOpenAlex(ctx, body.Query, body.MaxResults) + } + if channelEnabled(body.Channels, "dblp") { + channels["dblp"] = s.searchDBLP(ctx, body.Query, body.MaxResults) + } + if channelEnabled(body.Channels, "ieee") { + if key := envOr("IEEE_API_KEY", ""); key != "" { + if papers, err := core.FetchIEEE(ctx, key, body.Query, body.MaxResults); err == nil { + items := make([]map[string]any, len(papers)) + for i, p := range papers { + items[i] = map[string]any{ + "title": p.Title, "abstract": p.Abstract, + "publication_date": p.PublicationDate, + "metadata": map[string]any{"source": "ieee"}, + } + } + channels["ieee"] = items + } else { + channels["ieee"] = map[string]any{"error": err.Error()} + } + } else { + channels["ieee"] = map[string]any{"error": "IEEE_API_KEY 未配置"} + } + } + if channelEnabled(body.Channels, "biorxiv") { + channels["biorxiv"] = map[string]any{"error": "bioRxiv 渠道暂不支持关键词检索"} + } writeJSON(w, http.StatusOK, map[string]any{"query": body.Query, "channels": channels}) } @@ -425,3 +455,110 @@ func (s *Server) handleDailyReportGenerateOnly(w http.ResponseWriter, r *http.Re } writeJSON(w, http.StatusOK, map[string]any{"task_id": taskID, "job_id": jobID, "status": "queued"}) } + +// searchOpenAlex OpenAlex works 检索(无 key,礼貌池)。 +func (s *Server) searchOpenAlex(ctx context.Context, query string, limit int) any { + u := fmt.Sprintf("https://api.openalex.org/works?search=%s&per-page=%d&select=id,title,abstract_inverted_index,publication_year,doi", + urlQueryEscape(query), limit) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) + req.Header.Set("User-Agent", "PaperMind/2.1 (mailto:papermind@local)") + resp, err := httpDefaultClient.Do(req) + if err != nil { + return map[string]any{"error": err.Error()} + } + defer resp.Body.Close() + var parsed struct { + Results []struct { + ID string `json:"id"` + Title string `json:"title"` + Year int `json:"publication_year"` + DOI string `json:"doi"` + Abstract map[string][]int `json:"abstract_inverted_index"` + } `json:"results"` + } + if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { + return map[string]any{"error": err.Error()} + } + items := []map[string]any{} + for _, w := range parsed.Results { + if w.Title == "" { + continue + } + // 倒排索引 → 摘要文本 + words := make(map[int]string) + for word, poss := range w.Abstract { + for _, pos := range poss { + words[pos] = word + } + } + abstract := make([]string, 0, len(words)) + for i := 0; i < len(words); i++ { + if v, ok := words[i]; ok { + abstract = append(abstract, v) + } + } + abstractText := strings.Join(abstract, " ") + if len(abstractText) > 400 { + abstractText = abstractText[:400] + } + out := map[string]any{ + "title": w.Title, "abstract": abstractText, + "year": w.Year, "metadata": map[string]any{"source": "openalex"}, + } + if w.DOI != "" { + out["doi"] = strings.TrimPrefix(w.DOI, "https://doi.org/") + } + items = append(items, out) + } + return items +} + +// searchDBLP DBLP 出版物检索。 +func (s *Server) searchDBLP(ctx context.Context, query string, limit int) any { + u := fmt.Sprintf("https://dblp.org/search/publ/api?q=%s&h=%d&format=json", + urlQueryEscape(query), limit) + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) + req.Header.Set("User-Agent", "PaperMind/2.1") + resp, err := httpDefaultClient.Do(req) + if err != nil { + return map[string]any{"error": err.Error()} + } + defer resp.Body.Close() + var parsed struct { + Result struct { + Hits struct { + Hit []struct { + Info struct { + Title string `json:"title"` + Venue string `json:"venue"` + Year string `json:"year"` + DOI string `json:"doi"` + EE string `json:"ee"` + } `json:"info"` + } `json:"hit"` + } `json:"hits"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { + return map[string]any{"error": err.Error()} + } + items := []map[string]any{} + for _, h := range parsed.Result.Hits.Hit { + if h.Info.Title == "" { + continue + } + title := strings.TrimRight(h.Info.Title, ".") + items = append(items, map[string]any{ + "title": title, "abstract": "", + "year": h.Info.Year, "venue": h.Info.Venue, + "metadata": map[string]any{"source": "dblp", "ee": h.Info.EE}, + }) + } + return items +} + +func urlQueryEscape(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(s, " ", "+"), "&", "%26") +} + +var httpDefaultClient = &http.Client{Timeout: 30 * time.Second}