geoGo/internal/collect/wenxin.go

899 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package collect
import (
"context"
"fmt"
"geo/internal/config"
"github.com/gofiber/fiber/v2/log"
"regexp"
"strings"
"time"
"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, browser *rod.Browser, page *rod.Page) CollectorInterface {
collector := &WenxinCollector{
BaseCollector: NewBaseCollector(ctx, params, cfg, logger, browser, page),
}
// 设置文心一言的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) {
// 注意SetupDriver 和 Close 已由 Manager 管理,这里不再调用
if err := c.InitPage(); err != nil {
return nil, fmt.Errorf("页面初始化失败: %v", err)
}
c.Sleep(3)
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)
}
// 获取分享链接
shareLink := ""
link, _ := c.getShareLink()
if link != "" {
shareLink = link
}
return &CollectResult{
Answer: answer,
ShareLink: shareLink,
}, nil
}
// inputQuestion 输入问题
func (c *WenxinCollector) inputQuestion(question string) error {
c.LogInfo("输入问题...")
// 文心一言的输入框选择器 - 根据实际页面结构调整
inputSelectors := []string{
"[contenteditable='true']",
"div[contenteditable]",
".editable__T7WAW4uW",
"[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(2000) // 等待弹窗出现
c.Screenshot("after_share_icon_click")
// 步骤3: 在弹窗中查找shareContainer的div
c.LogInfo("步骤3: 查找包含'shareContainer'的div元素...")
var shareContainerDiv *rod.Element
// 重新获取所有div元素
allDivs, err = c.Page.Elements("div")
if err != nil {
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 {
return "", fmt.Errorf("未找到包含'shareContainer' class的div元素")
}
// 步骤4: 在shareContainer内查找genLink的button
c.LogInfo("步骤4: 在shareContainer容器内查找包含'genLink'的button...")
var genLinkBtn *rod.Element
buttons, err := shareContainerDiv.Elements("button")
if err != nil {
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 {
return "", fmt.Errorf("在shareContainer容器内未找到包含'genLink' class的button")
}
// 滚动到按钮位置
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
}