945 lines
27 KiB
Go
945 lines
27 KiB
Go
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
|
||
}
|