package collect import ( "context" "fmt" "geo/internal/config" "regexp" "strings" "time" "github.com/gofiber/fiber/v2/log" "github.com/atotto/clipboard" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) // Source 文章引用来源结构体 type Source struct { Title string `json:"name"` Url string `json:"url"` PlatformName string `json:"platform"` PlatformIcon string `json:"Platform_icon"` } // WenxinCollector 文心一言收集器 type WenxinCollector struct { *BaseCollector } // NewWenxinCollector 创建文心一言收集器 func NewWenxinCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger) CollectorInterface { collector := &WenxinCollector{ BaseCollector: NewBaseCollector(ctx, params, cfg, logger), } // 设置文心一言的URL collector.LoginURL = "https://passport.baidu.com/v2/?login" collector.ChatURL = "https://yiyan.baidu.com/" return collector } // SetupDriver 重写父类方法,添加中文语言设置 func (c *WenxinCollector) SetupDriver() error { if err := c.BaseCollector.SetupDriver(); err != nil { return err } return nil } // CheckLoginStatus 检查登录状态 func (c *WenxinCollector) CheckLoginStatus() bool { // 检查页面上是否存在内容为"登录"或"Login"的button,如果存在说明未登录 loginButtons, err := c.Page.Elements("button") if err == nil { for _, btn := range loginButtons { text, _ := btn.Text() trimmedText := strings.TrimSpace(text) if trimmedText == "登录" || trimmedText == "Login" { c.LogInfo(fmt.Sprintf("检测到页面上有'%s'按钮,说明未登录", trimmedText)) return false } } } // 如果没有找到"登录"或"Login"按钮,说明已登录 return true } // WaitLogin 等待登录 func (c *WenxinCollector) WaitLogin() (bool, string) { if err := c.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } defer c.Close() c.Page.MustNavigate(c.ChatURL) c.Sleep(3) if c.CheckLoginStatus() { c.SaveCookies() return true, "already_logged_in" } // 最多等待300秒 for i := 0; i < 300; i++ { if c.CheckLoginStatus() { c.Sleep(2) c.SaveCookies() return true, "login_success" } time.Sleep(1 * time.Second) } return false, "登录超时" } // AskQuestion 提问并获取答案 func (c *WenxinCollector) AskQuestion(question string) (*CollectResult, error) { if err := c.SetupDriver(); err != nil { return nil, fmt.Errorf("浏览器启动失败: %v", err) } defer c.Close() if err := c.InitPage(); err != nil { return nil, fmt.Errorf("页面初始化失败: %v", err) } if err := c.inputQuestion(question); err != nil { return nil, fmt.Errorf("输入问题失败: %v", err) } if err := c.clickSendButton(); err != nil { return nil, fmt.Errorf("点击发送按钮失败: %v", err) } answer, err := c.waitForAnswer() if err != nil { return nil, fmt.Errorf("获取答案失败: %v", err) } answerStr, isExposure := HighlightKeywordsInText(answer, c.KeyWords) // 获取分享链接 shareLink := "" //link, _ := c.getShareLink() //if link != "" { // shareLink = link //} return &CollectResult{ Answer: answerStr, ShareLink: shareLink, IsExposure: isExposure, }, nil } // inputQuestion 输入问题 func (c *WenxinCollector) inputQuestion(question string) error { c.LogInfo("输入问题...") // 文心一言的输入框选择器 - 根据实际页面结构调整 inputSelectors := []string{ "[contenteditable='true']", "div[contenteditable]", "[class*='editable']", } var inputBox *rod.Element var err error for _, selector := range inputSelectors { inputBox, err = c.WaitForElementVisible(selector, 10) if err == nil && inputBox != nil { c.LogInfo(fmt.Sprintf("找到输入框: %s", selector)) break } } if inputBox == nil { return fmt.Errorf("未找到输入框") } // 点击获取焦点 if err := inputBox.Click(proto.InputMouseButtonLeft, 1); err != nil { return fmt.Errorf("点击输入框失败: %v", err) } c.SleepMs(500) // fallback: 使用Focus + Input inputBox.Focus() c.SleepMs(200) inputBox.Input(question) c.LogInfo(fmt.Sprintf("问题已输入: %s", question)) c.SleepMs(1000) return nil } // clickSendButton 点击发送按钮 func (c *WenxinCollector) clickSendButton() error { c.LogInfo("点击发送按钮...") // 使用正则匹配包含"send"的class(防CSS混淆) allElements, err := c.Page.Elements("*") if err != nil { return fmt.Errorf("获取页面元素失败: %v", err) } var sendBtn *rod.Element for _, elem := range allElements { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "send") { // 检查是否是可点击的元素(button、div等) tagName, _ := elem.Property("tagName") if tagName.Str() == "BUTTON" || tagName.Str() == "DIV" { sendBtn = elem c.LogInfo(fmt.Sprintf("通过正则找到发送按钮: class=%s, tag=%s", *classAttr, tagName.Str())) break } } } if sendBtn == nil { // fallback: 尝试查找最后一个button buttons, _ := c.Page.Elements("button") if len(buttons) > 0 { sendBtn = buttons[len(buttons)-1] c.LogInfo("使用最后一个button作为发送按钮") } } if sendBtn == nil { return fmt.Errorf("未找到发送按钮") } c.SleepMs(500) // 滚动到可见区域 if err := sendBtn.ScrollIntoView(); err != nil { c.LogInfo(fmt.Sprintf("滚动失败: %v", err)) } c.SleepMs(300) // 点击发送按钮 c.LogInfo("执行点击...") if err := sendBtn.Click(proto.InputMouseButtonLeft, 1); err != nil { return fmt.Errorf("点击发送按钮失败: %v", err) } c.LogInfo("已点击发送按钮") c.SleepMs(1000) // 检测是否发送成功:检查send按钮是否消失或变成pause按钮 maxWaitTime := 10 // 最多等待10秒 for i := 0; i < maxWaitTime*2; i++ { // 检查是否存在pause开头的按钮(表示正在生成) pauseExists, err := c.hasPauseButton() if err == nil && pauseExists { c.LogInfo("✓ 检测到pause按钮,消息发送成功,AI正在回答...") return nil } // 检查send按钮是否还存在 sendExists, _ := c.hasSendButton() if !sendExists { c.LogInfo("✓ send按钮已消失,消息发送成功") return nil } c.SleepMs(500) } c.LogInfo("⚠ 无法确认消息是否发送成功,但已尽力尝试") return nil } // hasSendButton 检查是否存在send开头的按钮 func (c *WenxinCollector) hasSendButton() (bool, error) { allElements, err := c.Page.Elements("*") if err != nil { return false, err } for _, elem := range allElements { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "send") { tagName, _ := elem.Property("tagName") if tagName.Str() == "BUTTON" || tagName.Str() == "DIV" { return true, nil } } } return false, nil } // hasPauseButton 检查是否存在pause开头的按钮 func (c *WenxinCollector) hasPauseButton() (bool, error) { allElements, err := c.Page.Elements("*") if err != nil { return false, err } for _, elem := range allElements { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "pause") { tagName, _ := elem.Property("tagName") if tagName.Str() == "BUTTON" || tagName.Str() == "DIV" { return true, nil } } } return false, nil } // waitForAnswer 等待并获取答案(处理流式输出) func (c *WenxinCollector) waitForAnswer() (string, error) { c.LogInfo("等待AI回答...") timeout := 180 // 最大等待时间(秒),流式输出可能需要更长时间 startTime := time.Now() var lastAnswer string var stableCount int // 稳定计数器,连续N次内容不变则认为完成 const requiredStableCount = 5 // 需要连续5次内容不变才认为完成 isAnswering := false // 标记是否正在回答中 for time.Since(startTime).Seconds() < float64(timeout) { // 检查是否存在pause按钮(表示正在生成答案) pauseExists, _ := c.hasPauseButton() if pauseExists { if !isAnswering { c.LogInfo("检测到pause按钮,AI正在生成回答...") isAnswering = true } } else if isAnswering { // pause按钮消失,可能回答完成了 c.LogInfo("pause按钮消失,检查回答是否完成...") // 再等待几次确认内容稳定 if stableCount >= requiredStableCount && lastAnswer != "" { c.LogInfo(fmt.Sprintf("✓ AI回答完成,最终长度: %d 字符", len(lastAnswer))) return lastAnswer, nil } } // 直接通过ID查找答案容器 answerElem, err := c.Page.Element("#answer_text_id") var answerText string if err == nil && answerElem != nil { // 获取整个HTML内容 htmlContent, err := answerElem.HTML() if err == nil && len(strings.TrimSpace(htmlContent)) > 30 { // 清理HTML标签,只保留纯文本 answerText = CleanDivTags(htmlContent) c.LogInfo(fmt.Sprintf("找到答案容器,清理后文本长度: %d", len(answerText))) } else { // 如果HTML获取失败,尝试获取文本 textContent, _ := answerElem.Text() answerText = strings.TrimSpace(textContent) c.LogInfo(fmt.Sprintf("找到答案容器,文本长度: %d", len(answerText))) } } else { c.LogInfo("未找到#answer_text_id元素") } // 检查是否获取到答案 if answerText != "" && len(answerText) > 30 { // 检查内容是否稳定(流式输出完成) if answerText == lastAnswer { stableCount++ c.LogInfo(fmt.Sprintf("答案稳定中... (%d/%d), 长度: %d", stableCount, requiredStableCount, len(answerText))) // 如果pause按钮不存在且内容稳定,说明回答完成 if !pauseExists && stableCount >= requiredStableCount { c.LogInfo(fmt.Sprintf("✓ AI回答完成,最终长度: %d 字符", len(answerText))) return answerText, nil } } else { // 内容还在变化,重置计数器 stableCount = 0 lastAnswer = answerText if pauseExists { c.LogInfo(fmt.Sprintf("检测到流式输出,当前长度: %d 字符", len(answerText))) } } } c.SleepMs(1500) // 每1.5秒检查一次 // 每10秒输出一次等待状态 elapsed := int(time.Since(startTime).Seconds()) if elapsed > 0 && elapsed%10 == 0 { c.LogInfo(fmt.Sprintf("等待AI回答中... 已等待 %d 秒", elapsed)) } } return "", fmt.Errorf("等待答案超时(%d秒)", timeout) } // SafeElement 安全地获取元素 func (c *WenxinCollector) SafeElement(selector string) (*rod.Element, error) { exists, _, err := c.Page.Has(selector) if err != nil { return nil, err } if !exists { return nil, nil } return c.Page.Element(selector) } // getShareLink 获取分享链接 func (c *WenxinCollector) getShareLink() (string, error) { c.LogInfo("=== 开始获取分享链接 ===") // 步骤1: 先找到包含dialogCardBottom的div c.LogInfo("步骤1: 查找包含'dialogCardBottom'的div元素...") var dialogDiv *rod.Element allDivs, err := c.Page.Elements("div") if err != nil { return "", fmt.Errorf("获取页面div元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个div元素中查找包含'dialogCardBottom'的class", len(allDivs))) for _, elem := range allDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "dialogcardbottom") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到dialogCardBottom容器: tag=%s, class=%s", tagName.Str(), *classAttr)) dialogDiv = elem break } } if dialogDiv == nil { return "", fmt.Errorf("未找到包含'dialogCardBottom' class的div元素") } // 步骤2: 在这个div内部查找包含share的元素 c.LogInfo("步骤2: 在dialogCardBottom容器内查找包含'share'的元素...") var shareDiv *rod.Element // 获取该容器内的所有子元素 childDivs, err := dialogDiv.Elements("div") if err != nil { return "", fmt.Errorf("获取子div元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个子div元素中查找包含'share'的class", len(childDivs))) for _, elem := range childDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "share") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到目标元素: tag=%s, class=%s", tagName.Str(), *classAttr)) shareDiv = elem break } } if shareDiv == nil { // 如果没找到div,尝试查找其他类型的元素(如button、svg等) c.LogInfo("未在子div中找到,尝试查找其他元素类型...") // 尝试查找所有子元素 allChildren, _ := dialogDiv.Elements("*") for _, elem := range allChildren { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "share") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到目标元素: tag=%s, class=%s", tagName.Str(), *classAttr)) shareDiv = elem break } } } if shareDiv == nil { return "", fmt.Errorf("在dialogCardBottom容器内未找到包含'share' class的元素") } // 滚动到元素位置 c.LogInfo("滚动到分享图标位置...") if scrollErr := shareDiv.ScrollIntoView(); scrollErr != nil { c.LogInfo(fmt.Sprintf("滚动失败: %v", scrollErr)) } c.SleepMs(800) // 普通点击 c.LogInfo("执行普通点击...") if clickErr := shareDiv.Click(proto.InputMouseButtonLeft, 1); clickErr != nil { return "", fmt.Errorf("点击分享图标失败: %v", clickErr) } c.LogInfo("✓ 点击成功") c.SleepMs(3000) // 等待弹窗出现 c.Screenshot("after_share_icon_click") // 步骤3: 在弹窗中查找shareContainer的div(带重试机制) c.LogInfo("步骤3: 查找包含'shareContainer'的div元素...") var shareContainerDiv *rod.Element maxRetries := 5 retryDelay := 1000 // 每次重试间隔1秒 for attempt := 1; attempt <= maxRetries; attempt++ { c.LogInfo(fmt.Sprintf("第 %d/%d 次尝试查找shareContainer...", attempt, maxRetries)) // 重新获取所有div元素 allDivs, err = c.Page.Elements("div") if err != nil { c.LogInfo(fmt.Sprintf("获取页面div元素失败: %v", err)) if attempt < maxRetries { c.SleepMs(retryDelay) continue } return "", fmt.Errorf("获取页面div元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个div元素中查找包含'shareContainer'的class", len(allDivs))) for _, elem := range allDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "sharecontainer") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到shareContainer容器: tag=%s, class=%s", tagName.Str(), *classAttr)) shareContainerDiv = elem break } } if shareContainerDiv != nil { break // 找到了,退出重试循环 } // 没找到,等待后重试 if attempt < maxRetries { c.LogInfo(fmt.Sprintf("未找到shareContainer,%d毫秒后重试...", retryDelay)) c.SleepMs(retryDelay) } } if shareContainerDiv == nil { c.Screenshot("share_container_not_found") return "", fmt.Errorf("经过 %d 次重试仍未找到包含'shareContainer' class的div元素", maxRetries) } // 步骤4: 在shareContainer内查找genLink的button(带重试机制) c.LogInfo("步骤4: 在shareContainer容器内查找包含'genLink'的button...") var genLinkBtn *rod.Element maxRetries = 3 retryDelay = 800 for attempt := 1; attempt <= maxRetries; attempt++ { c.LogInfo(fmt.Sprintf("第 %d/%d 次尝试查找genLink按钮...", attempt, maxRetries)) buttons, err := shareContainerDiv.Elements("button") if err != nil { c.LogInfo(fmt.Sprintf("获取button元素失败: %v", err)) if attempt < maxRetries { c.SleepMs(retryDelay) continue } return "", fmt.Errorf("获取button元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个button元素中查找包含'genLink'的class", len(buttons))) for _, elem := range buttons { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "genlink") { tagName, _ := elem.Property("tagName") text, _ := elem.Text() c.LogInfo(fmt.Sprintf("✓ 找到genLink按钮: tag=%s, class=%s, text=%s", tagName.Str(), *classAttr, strings.TrimSpace(text))) genLinkBtn = elem break } } if genLinkBtn != nil { break // 找到了,退出重试循环 } // 没找到,等待后重试 if attempt < maxRetries { c.LogInfo(fmt.Sprintf("未找到genLink按钮,%d毫秒后重试...", retryDelay)) c.SleepMs(retryDelay) } } if genLinkBtn == nil { c.Screenshot("genlink_button_not_found") return "", fmt.Errorf("经过 %d 次重试仍未在shareContainer容器内找到包含'genLink' class的button", maxRetries) } // 滚动到按钮位置 c.LogInfo("滚动到genLink按钮位置...") if scrollErr := genLinkBtn.ScrollIntoView(); scrollErr != nil { c.LogInfo(fmt.Sprintf("滚动失败: %v", scrollErr)) } c.SleepMs(500) // 点击genLink按钮 c.LogInfo("点击genLink按钮...") if clickErr := genLinkBtn.Click(proto.InputMouseButtonLeft, 1); clickErr != nil { return "", fmt.Errorf("点击genLink按钮失败: %v", clickErr) } c.LogInfo("✓ genLink按钮点击成功") c.SleepMs(1500) // 等待复制链接完成 // 步骤5: 从剪贴板读取分享链接 c.LogInfo("步骤5: 从系统剪贴板读取分享链接...") clipboardText, err := clipboard.ReadAll() if err != nil { return "", fmt.Errorf("读取剪贴板失败: %v", err) } if clipboardText == "" { return "", fmt.Errorf("剪贴板内容为空") } c.LogInfo(fmt.Sprintf("剪贴板原始内容: %s", clipboardText)) // 使用正则表达式提取URL // 匹配 http:// 或 https:// 开头的URL re := regexp.MustCompile(`https?://[^\s]+`) matches := re.FindStringSubmatch(clipboardText) if len(matches) == 0 { return "", fmt.Errorf("未能从剪贴板内容中提取URL") } url := matches[0] c.LogInfo(fmt.Sprintf("✓✓✓ 成功获取分享链接: %s", url)) return url, nil } // GetSources 获取文章引用来源(前5个) func (c *WenxinCollector) GetSources() ([]Source, error) { c.LogInfo("=== 开始获取文章引用来源 ===") var sources []Source // 步骤1: 多层查找titleText的div c.LogInfo("步骤1: 查找roleSystem容器...") var roleSystemDiv *rod.Element allDivs, err := c.Page.Elements("div") if err != nil { return nil, fmt.Errorf("获取页面div元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个div元素中查找包含'roleSystem'的class", len(allDivs))) for _, elem := range allDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "rolesystem") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到roleSystem容器: tag=%s, class=%s", tagName.Str(), *classAttr)) roleSystemDiv = elem break } } if roleSystemDiv == nil { c.LogInfo("未找到roleSystem容器,结束获取") return sources, nil // 没有找到就返回空列表 } // 步骤2: 在roleSystem下查找container c.LogInfo("步骤2: 在roleSystem内查找包含'container'的div...") var containerDiv *rod.Element containerDivs, err := roleSystemDiv.Elements("div") if err != nil { return nil, fmt.Errorf("获取roleSystem子div元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个子div中查找包含'container'的class", len(containerDivs))) for _, elem := range containerDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "container") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到container容器: tag=%s, class=%s", tagName.Str(), *classAttr)) containerDiv = elem break } } if containerDiv == nil { c.LogInfo("未找到container容器,结束获取") return sources, nil } // 步骤3: 查找第二个container(在整个页面中查找所有container,取第二个) c.LogInfo("步骤3: 在页面中查找所有包含'container'的div,找到第二个...") var secondContainerDiv *rod.Element allDivs, err = c.Page.Elements("div") if err != nil { return nil, fmt.Errorf("获取页面div元素失败: %v", err) } containerCount := 0 for _, elem := range allDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "container") { containerCount++ if containerCount == 2 { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到第二个container容器: tag=%s, class=%s", tagName.Str(), *classAttr)) secondContainerDiv = elem break } } } if secondContainerDiv == nil { c.LogInfo(fmt.Sprintf("未找到第二个container容器(共找到 %d 个),结束获取", containerCount)) return sources, nil } // 步骤4: 在第二个container内查找titleText c.LogInfo("步骤4: 在第二个container内查找包含'titleText'的div...") var titleTextDiv *rod.Element titleTextDivs, err := secondContainerDiv.Elements("div") if err != nil { return nil, fmt.Errorf("获取第二个container的子div元素失败: %v", err) } c.LogInfo(fmt.Sprintf("在 %d 个子div中查找包含'titleText'的class", len(titleTextDivs))) for _, elem := range titleTextDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "titletext") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到titleText元素: tag=%s, class=%s", tagName.Str(), *classAttr)) titleTextDiv = elem break } } if titleTextDiv == nil { c.LogInfo("未找到titleText元素,结束获取") return sources, nil } // 点击titleText c.LogInfo("点击titleText元素...") if scrollErr := titleTextDiv.ScrollIntoView(); scrollErr != nil { c.LogInfo(fmt.Sprintf("滚动失败: %v", scrollErr)) } c.SleepMs(500) if clickErr := titleTextDiv.Click(proto.InputMouseButtonLeft, 1); clickErr != nil { return nil, fmt.Errorf("点击titleText失败: %v", clickErr) } c.LogInfo("✓ titleText点击成功") c.SleepMs(2000) // 等待侧边窗出现 c.Screenshot("after_titletext_click") // 步骤2: 查找SourcesViewer侧边窗 c.LogInfo("步骤2: 查找包含'SourcesViewer'的div元素...") var sourcesViewerDiv *rod.Element allDivs, err = c.Page.Elements("div") if err != nil { return nil, fmt.Errorf("获取页面div元素失败: %v", err) } for _, elem := range allDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "sourcesviewer") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到SourcesViewer容器: tag=%s, class=%s", tagName.Str(), *classAttr)) sourcesViewerDiv = elem break } } if sourcesViewerDiv == nil { return nil, fmt.Errorf("未找到SourcesViewer侧边窗") } // 步骤3: 在SourcesViewer内查找list容器 c.LogInfo("步骤3: 在SourcesViewer内查找包含'list'的div...") var listDiv *rod.Element listDivs, err := sourcesViewerDiv.Elements("div") if err != nil { return nil, fmt.Errorf("获取子div元素失败: %v", err) } for _, elem := range listDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "list") { tagName, _ := elem.Property("tagName") c.LogInfo(fmt.Sprintf("✓ 找到list容器: tag=%s, class=%s", tagName.Str(), *classAttr)) listDiv = elem break } } if listDiv == nil { return nil, fmt.Errorf("未找到list容器") } // 步骤4: 在list内查找所有item c.LogInfo("步骤4: 在list内查找包含'item'的div...") itemDivs, err := listDiv.Elements("div") if err != nil { return nil, fmt.Errorf("获取item元素失败: %v", err) } c.LogInfo(fmt.Sprintf("找到 %d 个item元素", len(itemDivs))) // 只处理前5个item maxItems := 5 if len(itemDivs) < maxItems { maxItems = len(itemDivs) } for i := 0; i < maxItems; i++ { item := itemDivs[i] c.LogInfo(fmt.Sprintf("\n--- 处理第 %d 个item ---", i+1)) source := Source{} // 查找titleInfo (标题) titleDivs, _ := item.Elements("div") for _, div := range titleDivs { classAttr, _ := div.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "title") { text, _ := div.Text() source.Title = strings.TrimSpace(text) c.LogInfo(fmt.Sprintf(" 标题: %s", source.Title)) break } } // 查找site_icon (图标URL) imgs, _ := item.Elements("img") for _, img := range imgs { classAttr, _ := img.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "site_icon") { srcAttr, _ := img.Attribute("src") if srcAttr != nil { source.PlatformIcon = *srcAttr c.LogInfo(fmt.Sprintf(" 图标: %s", source.PlatformIcon)) } break } } // 查找siteText (来源媒体名称) for _, div := range titleDivs { classAttr, _ := div.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "sitetext") { text, _ := div.Text() source.PlatformName = strings.TrimSpace(text) c.LogInfo(fmt.Sprintf(" 来源: %s", source.PlatformName)) break } } // 尝试获取跳转URL // 方法1: 查找item内的a标签 links, _ := item.Elements("a") if len(links) > 0 { href, _ := links[0].Attribute("href") if href != nil && *href != "" { source.Url = *href c.LogInfo(fmt.Sprintf(" URL (从href获取): %s", source.Url)) } } // 方法2: 如果没找到href,尝试点击item获取URL if source.Url == "" { c.LogInfo(" 未找到href,尝试点击item获取URL...") // 记录当前URL currentURL := c.Page.MustInfo().URL // 点击item if scrollErr := item.ScrollIntoView(); scrollErr != nil { c.LogInfo(fmt.Sprintf(" 滚动失败: %v", scrollErr)) } c.SleepMs(300) if clickErr := item.Click(proto.InputMouseButtonLeft, 1); clickErr != nil { c.LogInfo(fmt.Sprintf(" 点击item失败: %v", clickErr)) } else { c.SleepMs(2000) // 等待页面跳转 // 获取新URL newURL := c.Page.MustInfo().URL if newURL != currentURL { source.Url = newURL c.LogInfo(fmt.Sprintf(" URL (从跳转获取): %s", source.Url)) // 返回上一页 c.Page.MustNavigateBack() c.SleepMs(1500) // 等待返回 // 重新查找item元素(因为页面刷新了) c.LogInfo(" 重新查找item元素...") allDivs, _ = c.Page.Elements("div") for _, elem := range allDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "sourcesviewer") { sourcesViewerDiv = elem break } } if sourcesViewerDiv != nil { listDivs, _ = sourcesViewerDiv.Elements("div") for _, elem := range listDivs { classAttr, _ := elem.Attribute("class") if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "list") { listDiv = elem break } } if listDiv != nil { itemDivs, _ = listDiv.Elements("div") } } } } } // 添加到结果列表 if source.Title != "" || source.Url != "" { sources = append(sources, source) } } c.LogInfo(fmt.Sprintf("\n✓✓✓ 成功获取 %d 个引用来源", len(sources))) return sources, nil }