...
- bodyMarker := `result__body"`
- for i := 0; i < len(html); i++ {
- idx := strings.Index(html[i:], bodyMarker)
- if idx < 0 {
- break
- }
- i += idx
-
- // Find closing
- closeIdx := findClosingTag(html, i, "")
- if closeIdx < 0 {
- break
- }
- block := html[i : closeIdx+6]
-
- r := parseSingleDDGResult(block)
- if r.URL != "" {
- results = append(results, r)
- if len(results) >= count {
- break
- }
- }
-
- i = closeIdx + 6
- }
-
- return results
-}
-
-func findClosingTag(s string, start int, tag string) int {
- depth := 1
- pos := start
- for pos < len(s) {
- nextOpen := strings.Index(s[pos:], `= 0 && nextOpen < nextClose {
- depth++
- pos += nextOpen + 4
- } else {
- depth--
- if depth == 0 {
- return pos + nextClose
- }
- pos += nextClose + len(tag)
- }
- }
- return -1
-}
-
-func parseSingleDDGResult(block string) ddgResult {
- var r ddgResult
-
- // Extract URL and title from:
TITLE
- urlMarker := `class="result__a" href="`
- uIdx := strings.Index(block, urlMarker)
- if uIdx >= 0 {
- start := uIdx + len(urlMarker)
- end := strings.Index(block[start:], `"`)
- if end >= 0 {
- r.URL = block[start : start+end]
- }
-
- aStart := strings.Index(block[start+end:], `>`)
- if aStart >= 0 {
- titleStart := start + end + aStart + 1
- aEnd := strings.Index(block[titleStart:], ``)
- if aEnd >= 0 {
- r.Title = stripTags(block[titleStart : titleStart+aEnd])
- }
- }
- }
-
- // Extract snippet:
...
- snippetMarkers := []string{
- `
= 0 {
- aStart := strings.Index(block[sIdx:], `>`)
- if aStart >= 0 {
- snipStart := sIdx + aStart + 1
- snipEnd := strings.Index(block[snipStart:], ``)
- if snipEnd < 0 {
- snipEnd = strings.Index(block[snipStart:], `
`)
- }
- if snipEnd >= 0 {
- r.Snippet = stripTags(block[snipStart : snipStart+snipEnd])
- }
- }
- break
- }
- }
-
- return r
-}
-
-// ── Web Fetch ──────────────────────────────────────────────
-
-func (p *Plugin) handleFetch(args map[string]interface{}) (interface{}, error) {
- rawURL, _ := args["url"].(string)
- if rawURL == "" {
- return errorResult("url is required"), nil
- }
-
- maxChars := 20000
- if v, ok := args["max_chars"].(float64); ok && v > 0 {
- maxChars = int(v)
- }
- if maxChars > 500000 {
- maxChars = 500000
- }
-
- if err := p.ssrfCheck(rawURL); err != nil {
- return errorResult(err.Error()), nil
- }
-
- req, err := http.NewRequest("GET", rawURL, nil)
- if err != nil {
- return errorResult("invalid URL: " + err.Error()), nil
- }
- req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
-
- resp, err := p.client.Do(req)
- if err != nil {
- return errorResult("fetch failed: " + err.Error()), nil
- }
- defer resp.Body.Close()
-
- if resp.StatusCode < 200 || resp.StatusCode >= 400 {
- return errorResult(fmt.Sprintf("HTTP %d: %s", resp.StatusCode, resp.Status)), nil
- }
-
- body, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxChars)+50000))
- if err != nil {
- return errorResult("read error: " + err.Error()), nil
- }
-
- rawText := string(body)
-
- // Extract readable content based on content type
- ct := resp.Header.Get("Content-Type")
- var extracted string
- if strings.Contains(ct, "text/html") {
- extracted = htmlToText(rawText)
- } else if strings.Contains(ct, "application/json") {
- // Pretty-print JSON
- var v interface{}
- if json.Unmarshal(body, &v) == nil {
- if pretty, err := json.MarshalIndent(v, "", " "); err == nil {
- extracted = string(pretty)
- } else {
- extracted = rawText
- }
- } else {
- extracted = rawText
- }
- } else {
- extracted = rawText
- }
-
- // Clean up and truncate
- extracted = strings.TrimSpace(extracted)
- if len(extracted) > maxChars {
- extracted = extracted[:maxChars] + "\n\n[Content truncated]"
- }
-
- if extracted == "" {
- extracted = "(empty content)"
- }
-
- return map[string]interface{}{
- "content": extracted,
- "details": map[string]interface{}{
- "url": rawURL,
- "status": resp.StatusCode,
- "content_type": ct,
- },
- }, nil
-}
-
-// ── HTML → 文本 ──────────────────────────────────────────────
-
-func htmlToText(html string) string {
- // Remove scripts
- for {
- start := strings.Index(strings.ToLower(html), "