geoGo/internal/collect/qianwen.go

270 lines
6.2 KiB
Go
Raw Permalink 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"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
"github.com/gofiber/fiber/v2/log"
)
// QianwenCollector 通义千问收集器
type QianwenCollector struct {
*BaseCollector
}
// NewQianwenCollector 创建通义千问收集器
func NewQianwenCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) CollectorInterface {
collector := &QianwenCollector{
BaseCollector: NewBaseCollector(ctx, params, cfg, logger, browser, page),
}
// 设置通义千问的URL
collector.LoginURL = "https://tongyi.aliyun.com/qianwen/"
collector.ChatURL = "https://tongyi.aliyun.com/qianwen/"
return collector
}
// CheckLoginStatus 检查登录状态
func (c *QianwenCollector) CheckLoginStatus() bool {
currentURL := c.GetCurrentURL()
// 检查是否在通义千问页面
if strings.Contains(currentURL, "tongyi.aliyun.com") {
// 查找用户信息元素
userInfo, err := c.SafeElement(".user-avatar, .avatar, [class*='user'], [class*='profile']")
if err == nil && userInfo != nil {
return true
}
// 检查是否有输入框
inputBox, err := c.SafeElement("textarea, [contenteditable='true']")
if err == nil && inputBox != nil {
return true
}
}
return false
}
// WaitLogin 等待登录
func (c *QianwenCollector) 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"
}
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 *QianwenCollector) 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)
}
return &CollectResult{
Answer: answer,
ShareLink: "",
}, nil
}
// inputQuestion 输入问题
func (c *QianwenCollector) inputQuestion(question string) error {
// 通义千问的输入框选择器
inputSelectors := []string{
"textarea[placeholder*='输入']",
"textarea[placeholder*='问']",
"textarea",
"[contenteditable='true']",
".chat-input textarea",
"#chat-input",
".input-box textarea",
".question-input",
}
var inputBox *rod.Element
var err error
for _, selector := range inputSelectors {
inputBox, err = c.WaitForElementVisible(selector, 10)
if err == nil && inputBox != nil {
break
}
}
if inputBox == nil {
return fmt.Errorf("未找到输入框")
}
// 点击获取焦点
if err := inputBox.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击输入框失败: %v", err)
}
c.SleepMs(500)
// 清空输入框
if err := c.ClearInput(inputBox); err != nil {
// Ignore clear error
}
c.SleepMs(300)
// 输入问题
if err := c.SetInputValue(inputBox, question); err != nil {
inputBox.Input(question)
}
c.SleepMs(1000)
return nil
}
// clickSendButton 点击发送按钮
func (c *QianwenCollector) clickSendButton() error {
// 发送按钮选择器
sendSelectors := []string{
"button[class*='send']",
"button[class*='submit']",
".send-btn",
".submit-btn",
"button svg[path*='send']",
"[aria-label*='发送']",
".send-icon",
".submit-icon",
}
var sendBtn *rod.Element
var err error
for _, selector := range sendSelectors {
sendBtn, err = c.WaitForElementClickable(selector, 5)
if err == nil && sendBtn != nil {
break
}
}
if sendBtn == nil {
// 尝试通过SVG图标查找
sendBtn, err = c.Page.Element("button svg")
if err != nil {
return fmt.Errorf("未找到发送按钮")
}
}
c.SleepMs(500)
// 点击发送按钮
if err := c.JSClick(sendBtn); err != nil {
return fmt.Errorf("点击发送按钮失败: %v", err)
}
c.SleepMs(2000)
return nil
}
// waitForAnswer 等待并获取答案
func (c *QianwenCollector) waitForAnswer() (string, error) {
timeout := 120 // 最大等待时间(秒)
startTime := time.Now()
lastAnswerLength := 0
for time.Since(startTime).Seconds() < float64(timeout) {
// 查找答案区域
answerSelectors := []string{
".message-content",
".response-text",
"[class*='assistant'] [class*='content']",
"[class*='ai'] [class*='message']",
".chat-message.ai",
".answer-content",
".qianwen-answer",
}
for _, selector := range answerSelectors {
answerElements, err := c.Page.Elements(selector)
if err == nil && len(answerElements) > 0 {
// 获取最后一个答案元素
lastAnswer := answerElements[len(answerElements)-1]
visible, _ := lastAnswer.Visible()
if visible {
text, err := lastAnswer.Text()
if err == nil && len(strings.TrimSpace(text)) > 0 {
// 检查是否正在生成
isGenerating := strings.Contains(text, "正在") ||
strings.Contains(text, "思考中") ||
strings.Contains(text, "typing") ||
strings.Contains(text, "生成中")
if !isGenerating {
// 检查答案是否还在增长
currentLength := len(text)
if currentLength == lastAnswerLength && currentLength > 10 {
// 答案不再增长,认为已完成
return strings.TrimSpace(text), nil
}
lastAnswerLength = currentLength
}
}
}
}
}
c.SleepMs(1500)
}
return "", fmt.Errorf("等待答案超时")
}
// SafeElement 安全地获取元素
func (c *QianwenCollector) 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)
}