diff --git a/third_party/homeagent-sdk/example/browser/plg.json b/third_party/homeagent-sdk/example/browser/plg.json
index 2131638..86e1c10 100644
--- a/third_party/homeagent-sdk/example/browser/plg.json
+++ b/third_party/homeagent-sdk/example/browser/plg.json
@@ -2,7 +2,7 @@
"name": "browser",
"name_zh": "浏览器",
"name_en": "Browser",
- "version": "2.4.0",
+ "version": "2.4.1",
"description": "统一浏览器插件:搜索、HTTP抓取(quick)、无头渲染(normal)、交互式浏览器(interactive/CDP)",
"author": "HomeAgent",
"entry": "plugin.so",
diff --git a/third_party/homeagent-sdk/example/browser/plugin.go b/third_party/homeagent-sdk/example/browser/plugin.go
index bed747f..81b7662 100644
--- a/third_party/homeagent-sdk/example/browser/plugin.go
+++ b/third_party/homeagent-sdk/example/browser/plugin.go
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "html"
"io"
"log"
"net"
@@ -484,7 +485,9 @@ type searchResult struct {
}
func (p *Plugin) bingSearch(query string, count int) ([]searchResult, error) {
- u := fmt.Sprintf("https://www.bing.com/search?q=%s&count=%d", url.QueryEscape(query), count)
+ // 用 cn.bing.com:www.bing.com 对程序化请求常回 302(同意/重定向页),拿不到结果块。
+ // 另:Bing 忽略 count 参数,翻页靠 first=,这里保留 count 只为兼容旧调用语义。
+ u := fmt.Sprintf("https://cn.bing.com/search?q=%s&first=1&count=%d&setlang=zh-CN", url.QueryEscape(query), count)
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
@@ -494,38 +497,112 @@ func (p *Plugin) bingSearch(query string, count int) ([]searchResult, error) {
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
- return parseBingResults(string(body), count), nil
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Bing 返回 HTTP %d(%d 字节)", resp.StatusCode, len(body))
+ }
+ results := parseBingResults(string(body), count)
+ if len(results) == 0 {
+ // 关键:把「解析不出来」与「真的没结果」区分开。
+ // 以前两者都变成 "No results found.",版式一变就静默退化成「搜不到」。
+ return nil, fmt.Errorf("Bing 返回 %d 字节但未解析出结果(可能被反爬或版式变更,可改用 deepsearch 插件)", len(body))
+ }
+ return results, nil
}
-func parseBingResults(html string, count int) []searchResult {
+var (
+ bingBlockRe = regexp.MustCompile(`
]*>\s*]+href="([^"]+)"[^>]*>(.*?)`)
+ bingAnyLinkRe = regexp.MustCompile(`(?s)]+href="([^"]+)"[^>]*>(.*?)`)
+ bingSnipRe = regexp.MustCompile(`(?s)]*>(.*?)
`)
+ bingCaptionRe = regexp.MustCompile(`(?s)]*>(.*?)
`)
+)
+
+// splitBingBlocks 按块标记切分,每块内容延伸到下一个块标记为止。
+//
+// 不用 ``:结果块内部可能嵌套 (deep links),
+// 非贪婪匹配会在错误位置截断;而且块内第一个 往往是 Bing 的「来源行」,
+// 取到的是 `deepin.orghttps://www.deepin.org` 这种垃圾标题。
+func splitBingBlocks(pageHTML string) []string {
+ locs := bingBlockRe.FindAllStringIndex(pageHTML, -1)
+ if len(locs) == 0 {
+ return nil
+ }
+ blocks := make([]string, 0, len(locs))
+ for i, loc := range locs {
+ end := len(pageHTML)
+ if i+1 < len(locs) {
+ end = locs[i+1][0]
+ }
+ blocks = append(blocks, pageHTML[loc[1]:end])
+ }
+ return blocks
+}
+
+func parseBingResults(pageHTML string, count int) []searchResult {
+ if count <= 0 {
+ count = 5
+ }
var results []searchResult
- re := regexp.MustCompile(``)
- matches := re.FindAllStringSubmatch(html, -1)
- for _, m := range matches {
+ for _, block := range splitBingBlocks(pageHTML) {
if len(results) >= count {
break
}
- block := m[1]
- var r searchResult
- hrefRe := regexp.MustCompile(`]+href="([^"]+)"[^>]*>`)
- if hm := hrefRe.FindStringSubmatch(block); len(hm) > 1 {
- r.URL = hm[1]
+ // 标题:现代 Bing 是 ;没有 h2 时才退回到块内第一个链接。
+ var href, title string
+ if m := bingTitleRe.FindStringSubmatch(block); m != nil {
+ href, title = m[1], html.UnescapeString(stripTags(m[2]))
+ } else if m := bingAnyLinkRe.FindStringSubmatch(block); m != nil {
+ href, title = m[1], html.UnescapeString(stripTags(m[2]))
}
- titleRe := regexp.MustCompile(`]+href="[^"]+"[^>]*>(.*?)`)
- if tm := titleRe.FindStringSubmatch(block); len(tm) > 1 {
- r.Title = stripTags(tm[1])
+ href = bingRealURL(html.UnescapeString(href))
+
+ // 摘要:新版在 p.b_lineclamp*,旧版在 div.b_caption > p
+ var snippet string
+ if m := bingSnipRe.FindStringSubmatch(block); m != nil {
+ snippet = html.UnescapeString(stripTags(m[1]))
+ } else if m := bingCaptionRe.FindStringSubmatch(block); m != nil {
+ snippet = html.UnescapeString(stripTags(m[1]))
}
- snipRe := regexp.MustCompile(`.*?
(.*?)
`)
- if sm := snipRe.FindStringSubmatch(block); len(sm) > 1 {
- r.Snippet = stripTags(sm[1])
- }
- if r.URL != "" && r.Title != "" {
- results = append(results, r)
+
+ title, snippet = strings.TrimSpace(title), strings.TrimSpace(snippet)
+ if href == "" || title == "" || !strings.HasPrefix(href, "http") {
+ continue
}
+ results = append(results, searchResult{Title: title, URL: href, Snippet: snippet})
}
return results
}
+// bingRealURL 解开 Bing 的跳转包装:/ck/a?...&u=a1
&... → 真实 URL。
+// 不解的话模型拿到的是 `https://cn.bing.com/ck/a?...` 这种不可读地址。
+func bingRealURL(href string) string {
+ href = strings.TrimSpace(href)
+ if href == "" {
+ return ""
+ }
+ if !strings.Contains(href, "/ck/a") && !strings.Contains(href, "u=a1") {
+ return href
+ }
+ u, err := url.Parse(href)
+ if err != nil {
+ return href
+ }
+ raw := u.Query().Get("u")
+ if !strings.HasPrefix(raw, "a1") {
+ return href
+ }
+ b64 := raw[2:]
+ for _, enc := range []*base64.Encoding{base64.RawURLEncoding, base64.URLEncoding, base64.RawStdEncoding} {
+ if dec, err := enc.DecodeString(b64); err == nil {
+ s := string(dec)
+ if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
+ return s
+ }
+ }
+ }
+ return href
+}
+
func (p *Plugin) handleSearch(args map[string]interface{}) (interface{}, error) {
query := readArg(args, "query", "")
if query == "" {