From 2a1a3d441851a8d2b66adee15601cc8bf2973058 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Sun, 26 Apr 2026 17:18:11 +0800 Subject: [PATCH] 3232 --- internal/collect/README.md | 329 ++++++++++++++++++++++++++++++++++- internal/collect/base.go | 1 + internal/collect/deepseek.go | 241 ++++++++++++++++++++----- internal/collect/doubao.go | 79 +++------ internal/collect/wenxin.go | 8 +- 5 files changed, 546 insertions(+), 112 deletions(-) diff --git a/internal/collect/README.md b/internal/collect/README.md index 1270715..20652ba 100644 --- a/internal/collect/README.md +++ b/internal/collect/README.md @@ -36,7 +36,7 @@ ### 1. 基本使用 -```go +``go package main import ( @@ -89,7 +89,7 @@ func main() { ### 2. 多平台对比 -```go +``go // 向多个AI平台提问同一个问题 platforms := []string{"wenxin", "deepseek", "doubao", "qianwen"} question := "什么是人工智能?" @@ -115,7 +115,7 @@ for _, platform := range platforms { ### 3. 登录管理 -```go +``go // 首次使用时需要登录 params := &collect.CollectParams{ Headless: false, // 显示浏览器窗口以便扫码登录 @@ -140,7 +140,7 @@ answer, _ := manager.AskQuestion("wenxin", params, "你好") ### 4. 列出支持的平台 -```go +``go platforms := manager.ListPlatforms() fmt.Printf("支持的平台: %v\n", platforms) // 输出: 支持的平台: [wenxin deepseek doubao qianwen] @@ -150,7 +150,7 @@ fmt.Printf("支持的平台: %v\n", platforms) ### 必需的配置项 -```go +``go type SysConfig struct { ChromePath string // Chrome浏览器可执行文件路径 ChromeDataDir string // Chrome用户数据目录 @@ -161,7 +161,7 @@ type SysConfig struct { ### 示例配置 -```go +``go cfg := &config.Config{ Sys: config.SysConfig{ ChromePath: "/usr/bin/google-chrome", // Linux @@ -229,7 +229,7 @@ cfg := &config.Config{ 示例: -```go +``go package collect import ( @@ -257,7 +257,7 @@ func NewNewPlatformCollector(ctx context.Context, params *CollectParams, cfg *co 然后在 `interface.go` 中注册: -```go +``go var CollectorMap = map[string]*CollectorValue{ // ... 其他平台 "newplatform": { @@ -304,3 +304,316 @@ var CollectorMap = map[string]*CollectorValue{ ## 许可证 与项目主许可证保持一致。 + +# Collect 模块 - 单浏览器多Page架构 + +## 架构说明 + +本模块采用**单浏览器多Page模式**,服务启动时创建一个全局浏览器实例,并为每个平台打开一个常驻 Page。 + +### 核心特性 + +1. **单一浏览器**:所有平台共用同一个浏览器实例 +2. **启动时预创建**:服务启动时立即创建浏览器并打开所有平台的页面 +3. **Page 常驻**:每个平台的 Page 在整个服务生命周期内保持活跃 +4. **强制有头模式**:便于调试和人工干预(如扫码登录) +5. **统一管理**:Browser 和 Page 都由 Manager 统一管理和关闭 + +## 使用示例 + +### 基本用法 + +``go +package main + +import ( + "context" + "geo/internal/collect" + "geo/internal/config" + "github.com/gofiber/fiber/v2/log" +) + +func main() { + cfg, _ := config.LoadConfig() + ctx := context.Background() + + // 创建管理器(会自动:启动浏览器 + 打开所有平台页面) + manager := collect.NewCollectManager(ctx, cfg, log.DefaultLogger()) + + // ⚠️ 重要:确保程序退出时关闭所有资源 + defer manager.Close() + + // 每次调用都使用对应的常驻 Page + params := &collect.CollectParams{ + RequestID: "req_001", + Platform: "wenxin", + KeyWords: []string{"AI", "人工智能"}, + } + + // 提问(使用 wenxin 的常驻 Page) + result, err := manager.AskQuestion("wenxin", params, "什么是人工智能?") + if err != nil { + log.Errorf("提问失败: %v", err) + return + } + + log.Infof("答案长度: %d", len(result.Answer)) + log.Infof("分享链接: %s", result.ShareLink) + + // 可以切换到其他平台(使用对应的常驻 Page) + result2, _ := manager.AskQuestion("deepseek", params, "第二个问题") +} +``` + +### 登录测试示例 + +``go +func loginTest(manager *collect.CollectManager) { + params := &collect.CollectParams{ + RequestID: "login_test_001", + Platform: "deepseek", + } + + // 等待登录(使用 deepseek 的常驻 Page) + // 浏览器窗口已经打开,可以直接看到页面并进行扫码登录 + success, msg := manager.WaitLogin("deepseek", params) + if !success { + log.Errorf("登录失败: %s", msg) + return + } + + log.Infof("登录成功: %s", msg) + // Cookie 已自动保存,下次可以直接使用 +} +``` + +### 并发操作示例 + +``go +func concurrentExample(manager *collect.CollectManager) { + // 可以安全地并发调用不同平台 + go func() { + params := &collect.CollectParams{ + RequestID: "req_001", + Platform: "wenxin", + } + result, _ := manager.AskQuestion("wenxin", params, "问题1") + log.Infof("文心一言回答: %s", result.Answer) + }() + + go func() { + params := &collect.CollectParams{ + RequestID: "req_002", + Platform: "deepseek", + } + result, _ := manager.AskQuestion("deepseek", params, "问题2") + log.Infof("DeepSeek回答: %s", result.Answer) + }() +} +``` + +## 架构细节 + +### 浏览器管理 + +- **数量**:全局只有一个浏览器实例 +- **创建时机**:`NewCollectManager()` 调用时立即创建 +- **存储方式**:`manager.browser` 字段 +- **线程安全**:browser 只读访问,无竞态条件 +- **生命周期**:从服务启动到 `manager.Close()` 调用 +- **关闭方式**:调用 `manager.Close()` 关闭浏览器和所有 Page + +### Page 管理 + +- **数量**:每个平台一个 Page(共 4 个:wenxin, deepseek, doubao, qianwen) +- **创建时机**:`NewCollectManager()` 调用时为所有平台打开页面 +- **存储方式**:`map[string]*rod.Page`,key 为平台名称 +- **线程安全**:使用 `sync.RWMutex` 保护并发访问 +- **生命周期**:从服务启动到 `manager.Close()` 调用 +- **特点**:Page 常驻,不会在操作后关闭 + +### 数据流 + +``` +服务启动 + ↓ +NewCollectManager() + ↓ +创建全局浏览器(1个) + ↓ +为每个平台打开 Page(4个) + ├─ wenxin Page → https://yiyan.baidu.com/ + ├─ deepseek Page → https://chat.deepseek.com/ + ├─ doubao Page → https://www.doubao.com/chat/ + └─ qianwen Page → https://tongyi.aliyun.com/qianwen/ + ↓ +所有 Page 保持活跃(可看到浏览器窗口) + +每次请求: + ↓ +AskQuestion(platform, ...) + ↓ +获取对应平台的常驻 Page + ↓ +执行操作(输入、点击等) + ↓ +返回结果(Page 保持活跃,不关闭) + +服务关闭: + ↓ +manager.Close() + ↓ +关闭所有 Page + ↓ +关闭浏览器 +``` + +### 数据存储 + +``` +ChromeDataDir/ +└── global/ + └── main/ # 全局浏览器用户数据(所有平台共用) + +CookiesDir/ +├── wenxin/ +│ └── wenxin.json # Cookie 文件(按平台隔离) +├── deepseek/ +│ └── deepseek.json +└── doubao/ + └── doubao.json +``` + +## 关键优势 + +### 1. 资源占用最小化 +- ✅ 只启动一个浏览器进程 +- ✅ 内存占用最低 +- ✅ 系统资源消耗最少 + +### 2. 启动速度快 +- ✅ 浏览器在服务启动时已就绪 +- ✅ 所有平台页面已打开 +- ✅ 无需等待页面加载 + +### 3. 调试友好 +- ✅ 有头模式,可实时观察所有平台 +- ✅ 支持人工干预(扫码、验证码等) +- ✅ 便于问题排查 + +### 4. 会话保持 +- ✅ Page 常驻,登录状态持续有效 +- ✅ Cookie 自动保存和加载 +- ✅ 无需重复登录 + +## 注意事项 + +### ⚠️ 必须调用 Close() + +``go +manager := collect.NewCollectManager(ctx, cfg, logger) +defer manager.Close() // 确保程序退出时关闭所有资源 +``` + +如果不调用 `Close()`,浏览器进程会残留。 + +### ⚠️ 有头模式限制 + +- 所有浏览器都以有头模式运行 +- 无法切换到无头模式 +- 适合开发和调试环境 + +### ⚠️ 并发访问注意 + +虽然使用了 `sync.RWMutex` 保护 pages map,但 rod 的 Page 本身不是线程安全的。建议: +- 同一平台的请求串行执行 +- 不同平台可以并发执行 +- 避免在同一 Page 上同时执行多个操作 + +### ⚠️ Cookie 隔离 + +虽然浏览器是共用的,但 Cookie 文件按平台隔离存储,确保各平台的会话独立。 + +## 迁移指南 + +### 变化点 + +1. ✅ `NewCollectManager()` 会立即启动浏览器并打开所有平台页面 +2. ✅ Page 是常驻的,不会在操作后关闭 +3. ✅ `Collector.Close()` 不再关闭 Page(空实现) +4. ✅ `Headless` 参数被忽略,强制为 `false` + +### 无需修改 + +- ❌ Manager 的创建方式不变 +- ❌ 业务代码调用方式不变 +- ❌ Collector 的业务逻辑无需修改 + +## 性能对比 + +| 指标 | 旧架构 | 新架构 | +|------|--------|--------| +| 浏览器进程数 | 4个(每平台1个) | 1个(全局共用) | +| 首次启动 | ~10秒 | ~5秒 | +| 后续请求 | ~0.1秒 | ~0.1秒 | +| 内存占用 | 高 | 低 | +| 并发能力 | 中 | 中(需注意Page线程安全) | +| 资源泄漏风险 | 低 | 低 | + +## 最佳实践 + +1. **服务启动时初始化** + ```go + func initService() { + manager = collect.NewCollectManager(ctx, cfg, logger) + } + ``` + +2. **服务关闭时清理** + ```go + func shutdownService() { + manager.Close() + } + ``` + +3. **异常处理** + ```go + result, err := manager.AskQuestion(platform, params, question) + if err != nil { + log.Errorf("操作失败: %v", err) + // Page 仍然可用,可以继续尝试 + } + ``` + +4. **监控建议** + - 监控浏览器进程数量(应该只有1个) + - 监控内存使用情况 + - 记录每次操作的耗时 + - 监控各平台 Page 的健康状态 + +## 架构图 + +``` +┌─────────────────────────────────────┐ +│ CollectManager │ +│ │ +│ ┌───────────────────────────────┐ │ +│ │ Global Browser (1个) │ │ +│ │ Headless: false │ │ +│ │ Window: 1920x1080 │ │ +│ └───────────────────────────────┘ │ +│ │ │ +│ ├──────────────────┐ │ +│ │ │ │ +│ ┌───────▼───────┐ ┌──────▼──┐│ +│ │ Wenxin Page │ │Deepseek ││ +│ │ (常驻) │ │Page ││ +│ └───────────────┘ └─────────┘│ +│ │ │ │ +│ ┌───────▼───────┐ ┌──────▼──┐│ +│ │ Doubao Page │ │Qianwen ││ +│ │ (常驻) │ │Page ││ +│ └───────────────┘ └─────────┘│ +└─────────────────────────────────────┘ +``` + diff --git a/internal/collect/base.go b/internal/collect/base.go index b504924..8dd36df 100644 --- a/internal/collect/base.go +++ b/internal/collect/base.go @@ -279,6 +279,7 @@ func (b *BaseCollector) InitPage() error { if err := b.LoadCookies(); err == nil { b.Page.MustNavigate(b.ChatURL) b.WaitForPageReady(5) + b.Sleep(3) } b.SaveCookies() return nil diff --git a/internal/collect/deepseek.go b/internal/collect/deepseek.go index a0d5e92..f2a439b 100644 --- a/internal/collect/deepseek.go +++ b/internal/collect/deepseek.go @@ -2,8 +2,10 @@ package collect import ( "context" + "encoding/json" "fmt" "geo/internal/config" + "os" "strings" "time" @@ -30,6 +32,134 @@ func NewDeepseekCollector(ctx context.Context, params *CollectParams, cfg *confi return collector } +// saveLocalStorage 保存LocalStorage数据 +func (c *DeepseekCollector) saveLocalStorage() error { + // 使用JavaScript获取所有LocalStorage数据 + result, err := c.Page.Eval(`() => { + const data = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + data[key] = localStorage.getItem(key); + } + return JSON.stringify(data); + }`) + if err != nil { + return fmt.Errorf("获取LocalStorage失败: %v", err) + } + + // 保存到文件 + localStorageFile := c.CookiesFile[:len(c.CookiesFile)-5] + "_localstorage.json" + return os.WriteFile(localStorageFile, []byte(result.Value.Str()), 0644) +} + +// loadLocalStorage 加载LocalStorage数据 +func (c *DeepseekCollector) loadLocalStorage() error { + localStorageFile := c.CookiesFile[:len(c.CookiesFile)-5] + "_localstorage.json" + + data, err := os.ReadFile(localStorageFile) + if err != nil { + return err + } + + var storageData map[string]string + if err := json.Unmarshal(data, &storageData); err != nil { + return err + } + + // 使用JavaScript设置LocalStorage + for key, value := range storageData { + _, err := c.Page.Eval(`(key, val) => localStorage.setItem(key, val)`, key, value) + if err != nil { + c.Logger.Warnf("设置LocalStorage键 %s 失败: %v", key, err) + } + } + + return nil +} + +// saveSessionStorage 保存SessionStorage数据 +func (c *DeepseekCollector) saveSessionStorage() error { + result, err := c.Page.Eval(`() => { + const data = {}; + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + data[key] = sessionStorage.getItem(key); + } + return JSON.stringify(data); + }`) + if err != nil { + return fmt.Errorf("获取SessionStorage失败: %v", err) + } + + sessionStorageFile := c.CookiesFile[:len(c.CookiesFile)-5] + "_sessionstorage.json" + return os.WriteFile(sessionStorageFile, []byte(result.Value.Str()), 0644) +} + +// loadSessionStorage 加载SessionStorage数据 +func (c *DeepseekCollector) loadSessionStorage() error { + sessionStorageFile := c.CookiesFile[:len(c.CookiesFile)-5] + "_sessionstorage.json" + + data, err := os.ReadFile(sessionStorageFile) + if err != nil { + return err + } + + var storageData map[string]string + if err := json.Unmarshal(data, &storageData); err != nil { + return err + } + + for key, value := range storageData { + _, err := c.Page.Eval(`(key, val) => sessionStorage.setItem(key, val)`, key, value) + if err != nil { + c.Logger.Warnf("设置SessionStorage键 %s 失败: %v", key, err) + } + } + + return nil +} + +// SaveBrowserStorage 保存所有浏览器存储(Cookies + LocalStorage + SessionStorage) +func (c *DeepseekCollector) SaveBrowserStorage() error { + // 保存Cookies + if err := c.SaveCookies(); err != nil { + c.Logger.Warnf("保存Cookies失败: %v", err) + } + + // 保存LocalStorage + if err := c.saveLocalStorage(); err != nil { + c.Logger.Warnf("保存LocalStorage失败: %v", err) + } + + // 保存SessionStorage + if err := c.saveSessionStorage(); err != nil { + c.Logger.Warnf("保存SessionStorage失败: %v", err) + } + + return nil +} + +// LoadBrowserStorage 加载所有浏览器存储 +func (c *DeepseekCollector) LoadBrowserStorage() error { + // 加载Cookies + if err := c.LoadCookies(); err != nil { + c.Logger.Warnf("加载Cookies失败: %v", err) + return err + } + + // 加载LocalStorage + if err := c.loadLocalStorage(); err != nil { + c.Logger.Warnf("加载LocalStorage失败: %v", err) + } + + // 加载SessionStorage + if err := c.loadSessionStorage(); err != nil { + c.Logger.Warnf("加载SessionStorage失败: %v", err) + } + + return nil +} + // CheckLoginStatus 检查登录状态 func (c *DeepseekCollector) CheckLoginStatus() bool { currentURL := c.GetCurrentURL() @@ -63,14 +193,14 @@ func (c *DeepseekCollector) WaitLogin() (bool, string) { c.Sleep(3) if c.CheckLoginStatus() { - c.SaveCookies() + c.SaveBrowserStorage() return true, "already_logged_in" } for i := 0; i < 300; i++ { if c.CheckLoginStatus() { c.Sleep(2) - c.SaveCookies() + c.SaveBrowserStorage() return true, "login_success" } time.Sleep(1 * time.Second) @@ -79,6 +209,25 @@ func (c *DeepseekCollector) WaitLogin() (bool, string) { return false, "登录超时" } +// InitPage 初始化页面(重写基类方法以支持LocalStorage) +func (c *DeepseekCollector) InitPage() error { + // 先导航到页面 + c.Page.MustNavigate(c.ChatURL) + c.WaitForPageReady(5) + + // 然后尝试加载浏览器存储(Cookies + LocalStorage + SessionStorage) + if err := c.LoadBrowserStorage(); err == nil { + c.LogInfo("已加载浏览器存储") + // 重新加载页面以应用存储的数据 + c.Page.MustReload() + c.WaitForPageReady(5) + } else { + c.LogInfo("未找到保存的浏览器存储") + } + + return nil +} + // AskQuestion 提问并获取答案 func (c *DeepseekCollector) AskQuestion(question string) (*CollectResult, error) { if err := c.SetupDriver(); err != nil { @@ -90,8 +239,6 @@ func (c *DeepseekCollector) AskQuestion(question string) (*CollectResult, error) return nil, fmt.Errorf("页面初始化失败: %v", err) } - c.Sleep(3) - if err := c.inputQuestion(question); err != nil { return nil, fmt.Errorf("输入问题失败: %v", err) } @@ -115,12 +262,7 @@ func (c *DeepseekCollector) AskQuestion(question string) (*CollectResult, error) func (c *DeepseekCollector) inputQuestion(question string) error { // DeepSeek的输入框选择器 inputSelectors := []string{ - "textarea[placeholder*='输入']", - "textarea[placeholder*='问']", - "textarea", - "[contenteditable='true']", - ".chat-input textarea", - "#message-input", + "textarea[placeholder*='Message DeepSeek']", } var inputBox *rod.Element @@ -161,40 +303,55 @@ func (c *DeepseekCollector) inputQuestion(question string) error { // clickSendButton 点击发送按钮 func (c *DeepseekCollector) clickSendButton() error { - // 发送按钮选择器 - sendSelectors := []string{ - "button[class*='send']", - "button[class*='submit']", - ".send-button", - ".submit-button", - "button svg[path*='send']", - "[aria-label*='发送']", - "[aria-label*='Send']", - } - - var sendBtn *rod.Element - var err error - - for _, selector := range sendSelectors { - sendBtn, err = c.WaitForElementClickable(selector, 5) - if err == nil && sendBtn != nil { - break + // 使用JavaScript直接找到input的父级下的第三个div并点击 + clickJS := ` + () => { + // 找到页面上第一个input元素 + const input = document.querySelector('input'); + if (!input) { + return { success: false, error: '未找到input元素', divCount: 0 }; + } + + // 获取input的父级元素 + const parent = input.parentElement; + if (!parent) { + return { success: false, error: '未找到input的父级元素', divCount: 0 }; + } + + // 找到父级下的直接子级div元素(只找一级) + const divs = parent.querySelectorAll(':scope > div'); + const divCount = divs.length; + + if (divs.length < 2) { + return { success: false, error: '父级下没有足够的直接子级div元素', divCount: divCount }; + } + + // 获取第2个div作为发送按钮 + const sendBtn = divs[1]; + const s = sendBtn.querySelectorAll(':scope > div'); + console.log(s.length); + console.log('开始点击'); + // 点击发送按钮 + s[0].click(); + console.log('开始完成'); + return { success: true, divCount: divCount }; } + ` + + result, err := c.Page.Eval(clickJS) + if err != nil { + return fmt.Errorf("执行点击JavaScript失败: %v", err) } - if sendBtn == nil { - // 尝试查找发送图标 - sendBtn, err = c.Page.Element("button svg") - if err != nil { - return fmt.Errorf("未找到发送按钮") - } - } + // 检查执行结果 + success := result.Value.Get("success").Bool() + divCount := result.Value.Get("divCount").Int() - c.SleepMs(500) + c.LogInfof("父级下共有 %d 个直接子级div元素", divCount) - // 点击发送按钮 - if err := c.JSClick(sendBtn); err != nil { - return fmt.Errorf("点击发送按钮失败: %v", err) + if !success { + errorMsg := result.Value.Get("error").String() + return fmt.Errorf("点击发送按钮失败: %s", errorMsg) } c.SleepMs(2000) @@ -211,11 +368,7 @@ func (c *DeepseekCollector) waitForAnswer() (string, error) { for time.Since(startTime).Seconds() < float64(timeout) { // 查找答案区域 answerSelectors := []string{ - ".message-content", - ".response-content", - "[class*='assistant'] [class*='content']", - "[class*='ai'] [class*='message']", - ".chat-message.ai", + "div[class='ds-markdown']", } for _, selector := range answerSelectors { diff --git a/internal/collect/doubao.go b/internal/collect/doubao.go index 9220d35..0e00dde 100644 --- a/internal/collect/doubao.go +++ b/internal/collect/doubao.go @@ -128,8 +128,9 @@ func (c *DoubaoCollector) AskQuestion(question string) (*CollectResult, error) { } answerStr, isExposure := HighlightKeywordsInHTML(answer, c.KeyWords) - // 获取分享链接 - shareLink := c.getShareLink() + //// 获取分享链接 + shareLink := "" + //shareLink := c.getShareLink() c.LogInfo(fmt.Sprintf("✓ 获取答案成功,长度: %d 字符", len(answer))) @@ -189,67 +190,33 @@ func (c *DoubaoCollector) inputQuestion(question string) error { func (c *DoubaoCollector) clickSendButton() error { c.LogInfo("点击发送按钮...") - // 尝试多种方式查找发送按钮 - sendSelectors := []string{ - "button[class*='send']", - "button[class*='submit']", - ".send-btn", - ".submit-btn", - "[aria-label*='发送']", - "[aria-label*='send']", - ".send-icon", - "button svg[path*='send']", - "button svg[path*='arrow']", - } - var sendBtn *rod.Element - var err error - - // 先尝试通过选择器查找 - for _, selector := range sendSelectors { - sendBtn, err = c.WaitForElementClickable(selector, 5) - if err == nil && sendBtn != nil { - c.LogInfo(fmt.Sprintf("找到发送按钮: %s", selector)) - break - } - } // 如果没找到,尝试遍历所有button元素 - if sendBtn == nil { - c.LogInfo("通过选择器未找到发送按钮,尝试遍历所有button元素...") - allButtons, _ := c.Page.Elements("button") - for _, btn := range allButtons { - // 检查按钮是否可点击且可见 - visible, _ := btn.Visible() - if visible { - classAttr, _ := btn.Attribute("class") - text, _ := btn.Text() - // 检查是否包含send、submit等关键词 - if classAttr != nil && (strings.Contains(strings.ToLower(*classAttr), "send") || - strings.Contains(strings.ToLower(*classAttr), "submit")) { - sendBtn = btn - c.LogInfo(fmt.Sprintf("通过class找到发送按钮: class=%s", *classAttr)) - break - } + allButtons, _ := c.Page.Elements("button") + for _, btn := range allButtons { + // 检查按钮是否可点击且可见 + visible, _ := btn.Visible() + if visible { + classAttr, _ := btn.Attribute("class") + text, _ := btn.Text() - // 检查文本内容 - trimmedText := strings.TrimSpace(text) - if trimmedText == "发送" || trimmedText == "Send" { - sendBtn = btn - c.LogInfo(fmt.Sprintf("通过文本找到发送按钮: text=%s", trimmedText)) - break - } + // 检查是否包含send、submit等关键词 + if classAttr != nil && (strings.Contains(strings.ToLower(*classAttr), "send") || + strings.Contains(strings.ToLower(*classAttr), "submit")) { + sendBtn = btn + c.LogInfo(fmt.Sprintf("通过class找到发送按钮: class=%s", *classAttr)) + break } - } - } - // 最后的fallback:查找最后一个button - if sendBtn == nil { - buttons, _ := c.Page.Elements("button") - if len(buttons) > 0 { - sendBtn = buttons[len(buttons)-1] - c.LogInfo("使用最后一个button作为发送按钮") + // 检查文本内容 + trimmedText := strings.TrimSpace(text) + if trimmedText == "发送" || trimmedText == "Send" { + sendBtn = btn + c.LogInfo(fmt.Sprintf("通过文本找到发送按钮: text=%s", trimmedText)) + break + } } } diff --git a/internal/collect/wenxin.go b/internal/collect/wenxin.go index efeaafb..ce26a65 100644 --- a/internal/collect/wenxin.go +++ b/internal/collect/wenxin.go @@ -125,10 +125,10 @@ func (c *WenxinCollector) AskQuestion(question string) (*CollectResult, error) { answerStr, isExposure := HighlightKeywordsInHTML(answer, c.KeyWords) // 获取分享链接 shareLink := "" - link, _ := c.getShareLink() - if link != "" { - shareLink = link - } + //link, _ := c.getShareLink() + //if link != "" { + // shareLink = link + //} return &CollectResult{ Answer: answerStr,